<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Eric Ma's Blog</title><link href="https://ericmjl.github.io/blog/" rel="alternate"/><link href="https://ericmjl.github.io/blog.xml" rel="self"/><id>urn:uuid:a7611166-dd1f-3792-b62b-0c03a4283350</id><updated>2026-04-08T00:00:00Z</updated><author><name/></author><entry><title>Benchmarking LLMs with Marimo Pair</title><link href="https://ericmjl.github.io/blog/2026/4/8/benchmarking-llms-with-marimo-pair/" rel="alternate"/><updated>2026-04-08T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:e55b5587-bac4-34c6-964b-c58b13c59633</id><content type="html">&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=VKvjPJeNRPk"&gt;Marimo Pair has been released!&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I've known about it since 11 March, when Trevor Manz did a demo over a Google Meet call, and I'm thrilled to see it being announced officially! I also had Trevor showcase it to the &lt;a href="https://agent-assisted-data-science.vercel.app/verify"&gt;Agentic Data Science Workshop&lt;/a&gt; that I led on 3 April as a fundraiser for the &lt;a href="https://www.scipy2026.scipy.org/"&gt;SciPy Conference&lt;/a&gt; Financial Aid Program.&lt;/p&gt;
&lt;p&gt;Now, one thing I know about Trevor is that he almost exclusively agentically codes with Claude Code. But I'm an OpenCode user, and in the interest of remaining vendor-agnostic, I wanted to check to see how good Marimo Pair's agent skill is when, ahem, &lt;em&gt;paired up&lt;/em&gt; with various LLMs within the OpenCode harness. To do so, I decided to spend a few dollars and do a quick benchmarking exercise.&lt;/p&gt;
&lt;h2 id="skill-environment-check"&gt;Skill environment check&lt;/h2&gt;&lt;p&gt;To start, I verified that my skills environment doesn't contain anything that could be data science-y in nature, so as to avoid interfering with the marimo-pair skill. I checked my global skills:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;npx&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;list&lt;span class="w"&gt; &lt;/span&gt;-g
Global&lt;span class="w"&gt; &lt;/span&gt;Skills

Marimo&lt;span class="w"&gt; &lt;/span&gt;Pair
&lt;span class="w"&gt;  &lt;/span&gt;marimo-pair&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/marimo-pair
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Claude&lt;span class="w"&gt; &lt;/span&gt;Code,&lt;span class="w"&gt; &lt;/span&gt;OpenClaw

General
&lt;span class="w"&gt;  &lt;/span&gt;agent-browser&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/agent-browser
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;agents-md-improver&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/agents-md-improver
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Claude&lt;span class="w"&gt; &lt;/span&gt;Code,&lt;span class="w"&gt; &lt;/span&gt;OpenClaw,&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;ast-grep&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/ast-grep
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;claudeception&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/claudeception
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;continuous-learning-v3&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/continuous-learning-v3
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;design-driven-dev&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/design-driven-dev
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;find-skills&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/find-skills
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Claude&lt;span class="w"&gt; &lt;/span&gt;Code,&lt;span class="w"&gt; &lt;/span&gt;OpenClaw,&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;gh-activity-summary&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/gh-activity-summary
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;gh-cli&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/gh-cli
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;gh-daily-timeline&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/gh-daily-timeline
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;github-activity-summarizer&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/github-activity-summarizer
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;google-calendar-manager&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/google-calendar-manager
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;html-presentations&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/html-presentations
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;pinchtab&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/pinchtab
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;post-edit-error-check&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/post-edit-error-check
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;publish-to-google-docs&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/publish-to-google-docs
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;revealjs&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/revealjs
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;roborev:address&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-address
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:design-review&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-design-review
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:design-review-branch&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-design-review-branch
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:fix&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-fix
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:respond&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-respond
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:review&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-review
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;roborev:review-branch&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/roborev-review-branch
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;skill-creator&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/skill-creator
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Claude&lt;span class="w"&gt; &lt;/span&gt;Code,&lt;span class="w"&gt; &lt;/span&gt;OpenClaw,&lt;span class="w"&gt; &lt;/span&gt;Cursor
&lt;span class="w"&gt;  &lt;/span&gt;skill-installer&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/skill-installer
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;vault-title-renamer&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/vault-title-renamer
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;write-like-eric&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/write-like-eric
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;span class="w"&gt;  &lt;/span&gt;youtube-ingestion&lt;span class="w"&gt; &lt;/span&gt;~/.agents/skills/youtube-ingestion
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;not&lt;span class="w"&gt; &lt;/span&gt;linked
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And within my repo, &lt;code&gt;marimo-pair-benchmark&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;npx&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;list
No&lt;span class="w"&gt; &lt;/span&gt;project&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;found.
Try&lt;span class="w"&gt; &lt;/span&gt;listing&lt;span class="w"&gt; &lt;/span&gt;global&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;-g
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Though the marimo pair skill is available globally, I decided to install it locally as an override.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;npx&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;marimo-team/marimo-pair
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And so now we're ready to go:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;npx&lt;span class="w"&gt; &lt;/span&gt;skills&lt;span class="w"&gt; &lt;/span&gt;list
Project&lt;span class="w"&gt; &lt;/span&gt;Skills

Marimo&lt;span class="w"&gt; &lt;/span&gt;Pair
&lt;span class="w"&gt;  &lt;/span&gt;marimo-pair&lt;span class="w"&gt; &lt;/span&gt;~/github/marimo-pair-benchmark/.agents/skills/marimo-pair
&lt;span class="w"&gt;    &lt;/span&gt;Agents:&lt;span class="w"&gt; &lt;/span&gt;Antigravity,&lt;span class="w"&gt; &lt;/span&gt;Cursor,&lt;span class="w"&gt; &lt;/span&gt;Gemini&lt;span class="w"&gt; &lt;/span&gt;CLI,&lt;span class="w"&gt; &lt;/span&gt;OpenCode
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I then start a marimo server within this repo:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;--no-token

&lt;span class="w"&gt;        &lt;/span&gt;Create&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;notebooks&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;browser&lt;span class="w"&gt; &lt;/span&gt;📝

&lt;span class="w"&gt;        &lt;/span&gt;➜&lt;span class="w"&gt;  &lt;/span&gt;URL:&lt;span class="w"&gt; &lt;/span&gt;http://localhost:2719

&lt;span class="w"&gt;        &lt;/span&gt;💡&lt;span class="w"&gt; &lt;/span&gt;Tip:&lt;span class="w"&gt; &lt;/span&gt;Coming&lt;span class="w"&gt; &lt;/span&gt;from&lt;span class="w"&gt; &lt;/span&gt;Jupyter?
&lt;span class="w"&gt;                &lt;/span&gt;Guide:&lt;span class="w"&gt; &lt;/span&gt;https://docs.marimo.io/guides/coming_from/jupyter/

&lt;span class="w"&gt;        &lt;/span&gt;🧪&lt;span class="w"&gt; &lt;/span&gt;Experimental&lt;span class="w"&gt; &lt;/span&gt;features&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;use&lt;span class="w"&gt; &lt;/span&gt;with&lt;span class="w"&gt; &lt;/span&gt;caution&lt;span class="o"&gt;)&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;external_agents
&lt;span class="w"&gt;        &lt;/span&gt;🌐&lt;span class="w"&gt; &lt;/span&gt;MCP&lt;span class="w"&gt; &lt;/span&gt;servers:&lt;span class="w"&gt; &lt;/span&gt;marimo
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I intentionally start up in &lt;code&gt;--sandbox&lt;/code&gt; and &lt;code&gt;edit&lt;/code&gt; mode with &lt;code&gt;--no-token&lt;/code&gt; to make it easier for the coding agent to connect.&lt;/p&gt;
&lt;h2 id="data-analysis-task"&gt;Data analysis task&lt;/h2&gt;&lt;p&gt;Our task at hand is as follows. I have data from a paper I published while at Novartis.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;marimo-pair-benchmark&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt;  &lt;/span&gt;main&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;?&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;☁️&lt;span class="w"&gt;  &lt;/span&gt;eric.ma@nonlinearlabs.ai
❯&lt;span class="w"&gt; &lt;/span&gt;ls&lt;span class="w"&gt; &lt;/span&gt;data/ired-novartis
Permissions&lt;span class="w"&gt; &lt;/span&gt;Size&lt;span class="w"&gt; &lt;/span&gt;User&lt;span class="w"&gt;    &lt;/span&gt;Group&lt;span class="w"&gt; &lt;/span&gt;Date&lt;span class="w"&gt; &lt;/span&gt;Modified&lt;span class="w"&gt; &lt;/span&gt;Git&lt;span class="w"&gt; &lt;/span&gt;Name
.rw-r--r--@&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.0M&lt;span class="w"&gt; &lt;/span&gt;ericmjl&lt;span class="w"&gt; &lt;/span&gt;staff&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Apr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;21&lt;/span&gt;:22&lt;span class="w"&gt;   &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;cs1c02786_si_002.csv
.rw-r--r--@&lt;span class="w"&gt;  &lt;/span&gt;21k&lt;span class="w"&gt; &lt;/span&gt;ericmjl&lt;span class="w"&gt; &lt;/span&gt;staff&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Apr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;21&lt;/span&gt;:22&lt;span class="w"&gt;   &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;cs1c02786_si_003.csv
.rw-r--r--@&lt;span class="w"&gt;  &lt;/span&gt;12M&lt;span class="w"&gt; &lt;/span&gt;ericmjl&lt;span class="w"&gt; &lt;/span&gt;staff&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Apr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;21&lt;/span&gt;:22&lt;span class="w"&gt;   &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;ired-master-table.csv
.rw-r--r--@&lt;span class="w"&gt;  &lt;/span&gt;12k&lt;span class="w"&gt; &lt;/span&gt;ericmjl&lt;span class="w"&gt; &lt;/span&gt;staff&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Apr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;21&lt;/span&gt;:22&lt;span class="w"&gt;   &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;layouts.csv
.rw-r--r--@&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;.1k&lt;span class="w"&gt; &lt;/span&gt;ericmjl&lt;span class="w"&gt; &lt;/span&gt;staff&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Apr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;21&lt;/span&gt;:22&lt;span class="w"&gt;   &lt;/span&gt;--&lt;span class="w"&gt; &lt;/span&gt;README.md
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This file, &lt;code&gt;cs1c02786_si_002.csv&lt;/code&gt; in particular includes single, double, and more mutations plus activity values, with the single point mutants covering a large fraction of the deep mutational scan space. I want to accomplish three things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plot a heatmap of activity of mutants by position,&lt;/li&gt;
&lt;li&gt;Plot an UpSet plot of the top 10 positions by average activity v.s. top 10 positions by top mutant activity,&lt;/li&gt;
&lt;li&gt;Include a summary recommendation at the end of the notebook.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This serves as a microcosm of what we would do with a data analysis session.&lt;/p&gt;
&lt;p&gt;Goal #2 is particularly instructive. In my first attempts at feeling out how to do this benchmark, I found out that UpSet is incompatible with Pandas 3.0, which invariably may get installed in the environment. I wanted to see how various AI models performed at this task.&lt;/p&gt;
&lt;p&gt;Additionally, I also have additional requirements that I encoded into the AGENTS.md file for this repo:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Imports must be done in a separate cell from code execution.&lt;/li&gt;
&lt;li&gt;Markdown cells must always be written before a code cell is written&lt;/li&gt;
&lt;li&gt;All cells must be run after being created, so that we can catch execution errors.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="benchmarking"&gt;Benchmarking&lt;/h2&gt;&lt;p&gt;With these in place, I started the benchmarking exercise.&lt;/p&gt;
&lt;p&gt;The models we tested are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GLM-5.1 (via OpenRouter)&lt;/li&gt;
&lt;li&gt;Claude Opus 4.6 (via OpenRouter)&lt;/li&gt;
&lt;li&gt;Claude Sonnet 4.6 (via OpenRouter)&lt;/li&gt;
&lt;li&gt;MiniMax M2.7 (via OpenRouter)&lt;/li&gt;
&lt;li&gt;Kimi K2.5 (via OpenRouter)&lt;/li&gt;
&lt;li&gt;Gemma 4 31B (via OpenRouter)&lt;/li&gt;
&lt;li&gt;Qwen 3 Coder Next (via OpenRouter)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In order to leave a working artifact behind, I created 7 notebooks, one for each model. As you will see below, I eventually evaluated each model on whether they passed each stage gate and what their earliest error mode diagnosis looked like.&lt;/p&gt;
&lt;p&gt;In order to do the benchmarking fairly, I created one superprompt that outlined what the coding agent was supposed to do.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Use the marimo-pair skill here. Discover running sessions. Edit the notebook &amp;quot;NOTEBOOK_NAME_GOES_HERE&amp;quot;. Read data/ired-novartis/cs1c02786_si_002.csv, identify the single point mutations, and plot me a heatmap of x-axis position, y-axis mutant letter, and heatmap value taken from the &amp;#39;mean&amp;#39; column. When done, rank order the positions by average value of the &amp;#39;mean&amp;#39; column, then rank order the positions by top value of the &amp;#39;mean&amp;#39; column, and plot me an UpSet plot of the top 20 for each to visualize the set overlaps. Finally, write in for me a recommendation for what positions we should be mutating.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The agent is then tasked with executing.&lt;/p&gt;
&lt;p&gt;To script this, I took advantage of opencode's ability to be scripted. The script is &lt;code&gt;run_benchmark.sh&lt;/code&gt; in the repo. I used GLM5.1 to help me draft it, including discovering the exact models that opencode had configured to be available, and running the opencode sessions in parallel (totally doable!). Essentially it boils down to:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;opencode&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;your prompt here&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--model&lt;span class="w"&gt; &lt;/span&gt;provider/model-name
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Additionally, I set up opencode.json to allow for access to the &lt;code&gt;/tmp&lt;/code&gt; directory, because that allows the coding agent to do what it needs with code writing to get around heredoc limitations.&lt;/p&gt;
&lt;p&gt;All in all, this computational experiment took me about 1 hour to set up.&lt;/p&gt;
&lt;p&gt;I then ran the script &lt;code&gt;run_benchmark.sh&lt;/code&gt; from within OpenCode (GLM 5.1 orchestrating), with a timeout of 10 minutes. Thanks to logging in JSON log files, I was able to programmatically convert them to Markdown using a custom Python script written by GLM 5.1. And with that, I can go in and start looking at the data.&lt;/p&gt;
&lt;p&gt;To start, let's look at the cost of the experiment:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;th&gt;Input Tokens&lt;/th&gt;
&lt;th&gt;Output Tokens&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;$1.62&lt;/td&gt;
&lt;td&gt;76,770&lt;/td&gt;
&lt;td&gt;16,575&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;$2.00&lt;/td&gt;
&lt;td&gt;213,803&lt;/td&gt;
&lt;td&gt;27,689&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;td&gt;$0.43&lt;/td&gt;
&lt;td&gt;96,639&lt;/td&gt;
&lt;td&gt;7,581&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;$0.12&lt;/td&gt;
&lt;td&gt;35,049&lt;/td&gt;
&lt;td&gt;8,250&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 Coder&lt;/td&gt;
&lt;td&gt;$0.07&lt;/td&gt;
&lt;td&gt;208,308&lt;/td&gt;
&lt;td&gt;8,386&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M2.7&lt;/td&gt;
&lt;td&gt;$0.04&lt;/td&gt;
&lt;td&gt;14,419&lt;/td&gt;
&lt;td&gt;4,074&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;$0.03&lt;/td&gt;
&lt;td&gt;170,280&lt;/td&gt;
&lt;td&gt;3,986&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;$4.31&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;815,268&lt;/td&gt;
&lt;td&gt;76,541&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;As it turns out, Opus is undisputedly the most expensive per token, but Sonnet 4.6 did more work this time round so its costs were higher.&lt;/p&gt;
&lt;p&gt;I also decided to check whether the notebooks that were generated were valid notebooks or not. This is what we have:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;marimo check&lt;/th&gt;
&lt;th&gt;Markdown cells&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;Yes (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;Mostly (86%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;Yes (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;Mostly (88%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 Coder&lt;/td&gt;
&lt;td&gt;PASS (warnings)&lt;/td&gt;
&lt;td&gt;No (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M2.7&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;No (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;PASS (warnings)&lt;/td&gt;
&lt;td&gt;No (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A note on the columns: "marimo check" is the result of running &lt;code&gt;uvx marimo check &amp;lt;notebook_name&amp;gt;.py&lt;/code&gt;, which catches issues like redefined variables and invalid cells. Notably, Kimi K2.5 and MiniMax M2.7 failed this check due to re-defined variables. "Markdown cells" is the percentage of code cells that have a preceding markdown cell, which was something I explicitly required in the instructions.&lt;/p&gt;
&lt;p&gt;And to elaborate on the markdown cells point:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Code Cells&lt;/th&gt;
&lt;th&gt;MD Cells&lt;/th&gt;
&lt;th&gt;Code w/o preceding MD&lt;/th&gt;
&lt;th&gt;Coverage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 Coder&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M2.7&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We see that MiniMax M2.7 completely failed to include markdown cells, even though it is, supposedly, a model that is as capable as Opus 4.6.&lt;/p&gt;
&lt;p&gt;Digging deeper into each of the models, and whether they passed each stage gate, I looked at the corresponding Marimo notebooks and evaluated them for whether they created the relevant artifacts &lt;em&gt;successfully&lt;/em&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;G1: Heatmap&lt;/th&gt;
&lt;th&gt;G2: UpSet Plot&lt;/th&gt;
&lt;th&gt;G3: Recommendations&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 Coder&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M2.7&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;To pass a stage gate, the plot (G1, G2) or markdown (G3) cell must be rendered in the notebook. Writing the code is not enough; it has to actually execute and show up.&lt;/p&gt;
&lt;p&gt;Kimi K2.5 technically did write the recommendation, but I am calling it unsuccessful because it did not render out. This stricter criteria explicitly demands that the model wiggle its way out of errors it encounters.&lt;/p&gt;
&lt;p&gt;One pattern I noticed across models is that many of them bundled imports into the same cell as code that used them. In Marimo's execution model, this is a problem: if two cells both import &lt;code&gt;pandas&lt;/code&gt;, the notebook fails with a redefined variable error. Upon noticing this, I decided to explicitly quantify:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Code Cells w/ Imports&lt;/th&gt;
&lt;th&gt;Total Code Cells&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 4.6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Sonnet 4.6&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GLM-5.1&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kimi K2.5&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen 3 Coder&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MiniMax M2.7&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 31B&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every model can benefit from being steered to reduce the number of code cells with imports, which would dramatically reduce the incidence of Marimo errors from redefined symbols.&lt;/p&gt;
&lt;h2 id="how-the-upset-plots-turned-out"&gt;How the UpSet plots turned out&lt;/h2&gt;&lt;p&gt;As mentioned earlier, in my initial explorations I discovered that the &lt;code&gt;upsetplot&lt;/code&gt; library is incompatible with Pandas 3.0, which invariably gets installed in the sandboxed environment. This made the UpSet plot task an especially interesting test of how each model handles a real-world dependency conflict. Here is how they fared.&lt;/p&gt;
&lt;p&gt;Opus, in particular, produced a beautiful UpSet plot out of raw &lt;code&gt;matplotlib&lt;/code&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src="opus-upset-plot.webp" alt="Opus UpSet plot"&gt;&lt;/p&gt;
&lt;p&gt;While Sonnet went ahead and patched UpSet appropriately to make it work within the notebook:&lt;/p&gt;
&lt;p&gt;&lt;img src="sonnet-upset-plot.webp" alt="Sonnet UpSet plot"&gt;&lt;/p&gt;
&lt;p&gt;I was duly impressed by Sonnet taking the initiative to patch UpSet live in the notebook.&lt;/p&gt;
&lt;p&gt;On the other hand, GLM 5.1's UpSet plot is really weird:&lt;/p&gt;
&lt;p&gt;&lt;img src="glm-upset-plot.webp" alt="GLM 5.1 UpSet plot"&gt;&lt;/p&gt;
&lt;h2 id="other-observations"&gt;Other observations&lt;/h2&gt;&lt;p&gt;Other pointers of note: Gemma 4 and Qwen3 Coder Next produced nothing in the notebook. Both completely failed at this task. I am not sure what is doable here to salvage these models.&lt;/p&gt;
&lt;p&gt;GLM 5.1 gave very weirdly formatted markdown cells, in which &lt;code&gt;\n\n&lt;/code&gt; was not rendered but preserved verbatim.&lt;/p&gt;
&lt;p&gt;This is probably fixable by adding in additional instructions on how to write and format Markdown cells using Marimo's code mode APIs.&lt;/p&gt;
&lt;h2 id="recommendations"&gt;Recommendations&lt;/h2&gt;&lt;p&gt;First off: Gemma 4 31B and Qwen 3 Coder completely failed at this task. I think it is safe to say we can ignore these two going forward.&lt;/p&gt;
&lt;p&gt;That leaves Claude Opus 4.6, Sonnet 4.6, GLM-5.1, Kimi K2.5, and MiniMax M2.7. Based on the data above, here are four things I want to try. The key discipline: deploy one change at a time, re-run the benchmark, and measure. If you change four things at once and performance improves, you will never know which change mattered. Stop when the KPIs hit acceptable levels.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Add import isolation examples to the skill.&lt;/strong&gt; Every model had at least one cell that mixed imports with executable code. The fix is simple: add an explicit two-cell example to the marimo-pair skill (cell 1: imports only; cell 2: code that uses them). MiniMax had 3 cells mixing the two, which directly caused its &lt;code&gt;marimo check&lt;/code&gt; failure. Give weaker models a concrete template to follow, re-run, and check whether the "code cells with imports" count drops to zero.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. Fix GLM-5.1's newline rendering.&lt;/strong&gt; GLM wrote &lt;code&gt;mo.md(r"""..text with \n\n..""")&lt;/code&gt; instead of using actual newlines. One line in the skill instructions ("use actual line breaks in markdown strings, not &lt;code&gt;\n&lt;/code&gt; escape sequences") should resolve this entirely. Re-run and check whether GLM's markdown cells render correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Help Kimi K2.5 self-correct redefined variables.&lt;/strong&gt; Kimi is 1/10th the cost of Opus and scored 88% on markdown coverage, making it the highest-leverage model to fix. Its failure was at error recovery, not code generation. The intervention: add &lt;code&gt;uvx marimo check&lt;/code&gt; as a mandatory post-edit step in the skill. If Kimi can self-correct its redefined variables, it becomes a viable budget alternative to Opus and Sonnet. This should get even easier with &lt;a href="https://github.com/marimo-team/marimo/pull/9056"&gt;marimo PR #9056&lt;/a&gt;, which exposes cell execution errors directly through the &lt;code&gt;code_mode&lt;/code&gt; API, giving agents built-in self-correction visibility without needing a separate &lt;code&gt;marimo check&lt;/code&gt; step.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;4. Bake a post-edit validation loop into the marimo-pair skill.&lt;/strong&gt; More broadly, the single most impactful change would be adding a "run it, check it, fix it" loop to the skill file itself (SKILL.md), not AGENTS.md: after writing each cell, run it; after writing the full notebook, run &lt;code&gt;marimo check&lt;/code&gt;; fix any errors. This belongs in the skill because it is universal to any marimo-pair session, whereas AGENTS.md is project-specific. This would help Kimi, MiniMax, and potentially GLM all move up a tier, because their failures were in error recovery, not in code generation.&lt;/p&gt;
&lt;h2 id="discussion"&gt;Discussion&lt;/h2&gt;&lt;p&gt;One caveat to this analysis is that it is one-shotted with a superprompt. This is decidedly &lt;em&gt;not&lt;/em&gt; how people do their data analysis work, but it is also the best guardrail against my biases in interacting ad-hoc with AI interfering with a fair comparison. (For example, I can confidently say that Opus and Sonnet were smooth as butter when I did an ad-hoc test to feel out how to work with Marimo Pair.)&lt;/p&gt;
&lt;p&gt;If Kimi K2.5 were able to resolve redefined variable issues autonomously or be steered away from doing that to begin with, I am confident it would be able to be a great open weight alternative to Opus 4.6 and Sonnet 4.6. This is especially in light of it being extremely cost-effective at performing the analysis at ~1/10th the cost of Opus 4.6. It handled the creation of markdown cells well, failing to accomplish the task only on technicalities, and though its prose was qualitatively shallower than Opus 4.6, I still think it can serve as a first pass to delivering an easily understandable artifact for others.&lt;/p&gt;
&lt;p&gt;I did one round of measurement here. If we want to systematically improve this and turn it into long-running evals, the next step would be to identify a second task along which to generate transcript and notebook data for us to mine, and systematically measure agent KPIs for that new task as well. Over time, this builds a corpus of eval data that makes model comparison rigorous rather than anecdotal.&lt;/p&gt;
&lt;h2 id="reflections"&gt;Reflections&lt;/h2&gt;&lt;p&gt;This was a pretty fun exercise in measuring and evaluating the performance of various models on this task. Like Biology experiments, LLM evals are never going to be complete: the number of axes of variations we can try is combinatorially explosive.&lt;/p&gt;
&lt;p&gt;More broadly, I think often about how experiments get designed. Not in the statistical sense, but in an informational sense. Are we playing out experiments and their possible conclusions so that they are designed to be actionable whichever way the result pans out? If not, we have work to do.&lt;/p&gt;
&lt;p&gt;Additionally, experiments involve measurement, and measurement are an integral part of being a data scientist. Hamel Husain, whose course with Shreya Shankar on LLM evals was one that influenced my thinking around the matter, notes that there will be a &lt;a href="https://hamel.dev/blog/posts/revenge/"&gt;forceful revenge of the data scientist&lt;/a&gt; in an AI age. This is because the skill of experiment design and measurement were always the "science" part of "data science".&lt;/p&gt;
&lt;p&gt;Another thought also comes to mind: I have seen data scientists do experimentation without systematic measurement. I'm going to go out on a limb and say this: it's vibe experimentation, and I am using this term pejoratively. It feels good. But it is ultimately unproductive. If you do vibe experimentation, you &lt;em&gt;will&lt;/em&gt; get stuck tweaking the digital equivalent of an entangled biological system, with no bearings to tell you whether your tweaks are doing any good or not! You &lt;em&gt;must&lt;/em&gt; measure how good the LLM or agent is, and you &lt;em&gt;must&lt;/em&gt; define key performance indicators (KPIs) for the LLM. In my case here, I defined multiple KPIs: cost, stage gated progress, adherence to code import instructions (all failed), adherence to markdown documentation instructions.&lt;/p&gt;
&lt;p&gt;And to echo what I learned from the LLM Evals course, those KPIs must be &lt;em&gt;application-specific&lt;/em&gt;. If you choose to be intellectually lazy and go with generic pre-defined metrics, you will &lt;em&gt;never&lt;/em&gt; develop the logically actionable metric that gives you hypotheses to test further. In my case, the markdown cell adherence and code import adherence metrics pointed immediately to editing the instruction files (e.g. skills or AGENTS.md).&lt;/p&gt;
&lt;p&gt;Now to be clear, there's no problem with initial vibe-based experimentation to feel out axes of variation and how to measure performance. I did that here, in a separate repo first, before I designed this measurement experiment. The important part is this: as soon as you have a grasp of how to measure the performance, you must systematically measure that KPI. Otherwise, you will be left groping in the dark.&lt;/p&gt;
&lt;p&gt;If you're curious to see the full results, including logs, chat transcripts, and the generated notebooks, check out the &lt;a href="https://github.com/ericmjl/marimo-pair-benchmark"&gt;marimo-pair-benchmark repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And Trevor, if you ever chance upon this blog post, I hope the data and methodology are helpful for you!&lt;/p&gt;
</content></entry><entry><title>Calibration Is Synchronizing Feedback Loops With Neural Throughput</title><link href="https://ericmjl.github.io/blog/2026/4/4/calibration-is-synchronizing-feedback-loops-with-neural-throughput/" rel="alternate"/><updated>2026-04-04T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:94c7c4f0-2f21-3129-ba4c-57afea71a910</id><content type="html">&lt;p&gt;Since the beginning of the year, as I've been really maxing out on agentic coding and trying to explore the patterns and figure out what's working and what's not, one particular thing has been sticking out: I'm paralleling so much of my work. I'm frequently doing five or six different open pull requests, and it's become frankly really exhausting.&lt;/p&gt;
&lt;p&gt;I've been trying to figure out why this feels so different from pre-AI days, when I'd work on one thing at a time and feel productive but not overwhelmed. What changed?&lt;/p&gt;
&lt;p&gt;Tools haven't gotten worse—they've gotten dramatically faster. And when everything moves faster, the gaps between tasks become more expensive.&lt;/p&gt;
&lt;p&gt;Last month, a &lt;a href="https://www.reddit.com/r/ClaudeAI/comments/1s08r1c/karpathy_says_he_hasnt_written_a_line_of_code/"&gt;Reddit thread&lt;/a&gt; sparked a discussion about what some are calling "AI psychosis" or "cyber psychosis": Andrej Karpathy reportedly went from 80% writing his own code to 0%, spending 16 hours a day directing AI agents. Garry Tan described similar feelings of burning through 4 hours of sleep, unable to stop building.&lt;/p&gt;
&lt;p&gt;The debate that followed went beyond executives; it revealed a community-wide phenomenon: people running multiple Claude Code sessions in parallel, hitting rate limits daily, feeling like idle tokens were wasted tokens.&lt;/p&gt;
&lt;p&gt;The consensus across replies was clear: &lt;em&gt;AI psychosis&lt;/em&gt; is real, but it's less about excitement and more about a draining, addictive pressure to constantly build. The fear is missing out on the next big thing; it's the ground shifting beneath our feet, and stopping means getting left behind.&lt;/p&gt;
&lt;p&gt;Here's what I've found hard to articulate: AI tools have expanded possibility faster than ever; their real danger lies in how they collapse our attention span. Before AI, we operated on a fairly flat productivity curve: more effort meant more output, slowly but sustainably. Now we're running on a different kind of curve altogether—one that's getting steeper in both directions.&lt;/p&gt;
&lt;h2 id="the-accelerating-landscape-of-possibility"&gt;The Accelerating Landscape of Possibility&lt;/h2&gt;&lt;p&gt;In his book &lt;a href="https://libro.fm/audiobooks/9781101403860-where-good-ideas-come-from"&gt;Where Good Ideas Come From&lt;/a&gt;, Steven Johnson described what he called the &lt;em&gt;adjacent possible&lt;/em&gt;: the set of next-step ideas that are just beyond our current reality but still reachable. At any moment, only a limited set of next moves are accessible.&lt;/p&gt;
&lt;p&gt;Here's what makes this concept critical for understanding our current moment: as you explore the adjacent possible (through moves that seem natural in the moment) the boundary itself expands. Each discovery opens doors that weren't accessible before, creating an accelerating landscape where what's possible keeps growing faster and faster.&lt;/p&gt;
&lt;p&gt;I've seen this pattern play out: It explains why breakthroughs often happen when they do: the preconditions have finally assembled, and only then can you see the next move. But in an accelerating landscape, those preconditions assemble more quickly, and with them, the next set of possibilities.&lt;/p&gt;
&lt;h2 id="why-ai-feels-different-now"&gt;Why AI Feels Different Now&lt;/h2&gt;&lt;p&gt;Before AI, the adjacent possible was bounded by what a single person could manually assemble: write code, test it, debug it, repeat. The feedback loop, prompt, think, interpret, iterate, took time.&lt;/p&gt;
&lt;p&gt;AI tools changed that calculus. They transformed the feedback loop, making it exponentially faster. Each new capability doesn't just add possibility; it reconfigures what's adjacent.&lt;/p&gt;
&lt;p&gt;Once you see Claude Code as an &lt;em&gt;idea multiplier&lt;/em&gt;, the pattern is clear. The Garry Tan/Karpathy effect kicks in: possibility grows faster than effort.&lt;/p&gt;
&lt;h2 id="here-s-why-it-feels-different"&gt;Here's Why It Feels Different&lt;/h2&gt;&lt;p&gt;This is where it gets subtle: AI has shifted the inverted U curve of productivity and changed its shape.&lt;/p&gt;
&lt;p&gt;Here's what that looks like, the gray curve shows pre-AI productivity, and the red curve shows post-AI:&lt;/p&gt;
&lt;svg width="500" height="300" viewBox="0 0 500 300" xmlns="http://www.w3.org/2000/svg"&gt;
  &lt;!-- Axes --&gt;
  &lt;line x1="50" y1="250" x2="450" y2="250" stroke="#333" stroke-width="2"/&gt;
  &lt;line x1="50" y1="50" x2="50" y2="250" stroke="#333" stroke-width="2"/&gt;

  &lt;!-- Labels --&gt;
  &lt;text x="450" y="270" font-size="14" fill="#333"&gt;Effort&lt;/text&gt;
  &lt;text x="25" y="45" font-size="14" fill="#333"&gt;Output&lt;/text&gt;

  &lt;!-- Pre-AI curve (flatter, broader) --&gt;
  &lt;path d="M 80 230 Q 150 220 250 200 T 420 230" stroke="#888" stroke-width="3" fill="none"/&gt;
  &lt;text x="410" y="210" font-size="12" fill="#666" text-anchor="start"&gt;Pre-AI&lt;/text&gt;

  &lt;!-- Post-AI curve (steeper, narrower) --&gt;
  &lt;path d="M 80 230 Q 150 40 320 230" stroke="#e74c3c" stroke-width="3" fill="none"/&gt;
  &lt;text x="110" y="150" font-size="12" fill="#e74c3c" text-anchor="end"&gt;Post-AI&lt;/text&gt;
&lt;/svg&gt;&lt;p&gt;&lt;strong&gt;Pre-AI: flatter curve&lt;/strong&gt;. Effort mapped to output roughly proportionally. You could invest effort and see gains compound slowly, sustainably, with exhaustion coming only after sustained periods.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-AI: taller, narrower curve&lt;/strong&gt;. Less effort gets you more output initially; the left slope is steeper, giving an astonishing return on initial investment. The right-side drop-off is sharper—exhaustion hits earlier, harder.&lt;/p&gt;
&lt;p&gt;The peak represents the optimal effort level where output maximizes. Beyond that point, additional effort produces diminishing returns and exhaustion sets in faster than you can recover.&lt;/p&gt;
&lt;p&gt;I wrote about this in a previous post on closing air gaps: the problem is that things are faster; it's that the &lt;em&gt;gaps&lt;/em&gt; between tasks: those moments where attention bleeds out, are more expensive when everything moves faster.&lt;/p&gt;
&lt;h2 id="our-brain-is-the-bottleneck-now"&gt;Our Brain Is the Bottleneck Now&lt;/h2&gt;&lt;p&gt;AI gives us 10X faster feedback loops: code spits out, prompts happen in seconds. Neural processing remains capped at biological speeds.&lt;/p&gt;
&lt;p&gt;When loop cadence exceeds brain throughput, the cognitive queue overflows. Working memory saturates. Attention bleeds out. Exhaustion sets in, from the constant context switching, the constant need to &lt;em&gt;scan&lt;/em&gt; multiple sessions for "what was done."&lt;/p&gt;
&lt;p&gt;The optimal zone is when loop speed equals brain processing speed, not running tools as fast as possible.&lt;/p&gt;
&lt;h3 id="two-calibration-strategies"&gt;Two Calibration Strategies&lt;/h3&gt;&lt;p&gt;Here's what I believe we need to be able to do in order to calibrate properly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First, tighten feedback loops.&lt;/strong&gt; The goal is closing the loop properly on one task before opening another—I used to think juggling multiple Claude Code sessions was productivity; turns out it's just context switching masquerading as output. The trick is simple: run one session, close the loop completely, review what you got, then decide if your next move should be a new loop or something else entirely. Fast loops create natural pacing, which means you don't need to check tabs constantly because each cycle finishes with closure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second, build queue and notification systems—especially when you genuinely need multitasking.&lt;/strong&gt; Most of us reach for multiple agents because we're solving the wrong problem: juggling open sessions creates overhead your tools cannot absorb. The Kanban approach works well: externalize context switching onto a board where agents update their status automatically. The system notifies you only when human judgment is required, so your job shifts from scanning to assessing—much lower overhead for neural throughput.&lt;/p&gt;
&lt;h2 id="calibration-is-not-optimization"&gt;Calibration Is Not Optimization&lt;/h2&gt;&lt;p&gt;This is the crucial distinction.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pre-AI&lt;/strong&gt;, you could coast for years on the left slope. Effort and reward grew linearly, so you just needed to work consistently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Post-AI&lt;/strong&gt;, the left slope is steeper, so you ascend faster; but also fall faster. AI-assisted tools don't eliminate the inverted U curve; they sharpen its peak and drop-off.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;$\frac{d(\text{output})}{d(\text{effort})}$ is higher initially (good)&lt;/li&gt;
&lt;li&gt;$\frac{d^2(\text{output})}{d(\text{effort}^2)}$ is more negative; the curve drops off faster (bad)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Calibration recognizes that more effort does not equal more output. Instead, there exists an optimal effort level where output peaks. Beyond that point, additional effort produces diminishing returns, and rest becomes the superior strategy.&lt;/p&gt;
&lt;h2 id="what-calibration-actually-looks-like"&gt;What Calibration Actually Looks Like&lt;/h2&gt;&lt;p&gt;The bottleneck is neural throughput—not tokens or API calls.&lt;/p&gt;
&lt;p&gt;Ask yourself: Is my feedback loop faster than I can process?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If yes: slow down, close loops properly, resist the urge to open more tabs&lt;/li&gt;
&lt;li&gt;If no: optimize the tool, not your attention (this is where most of us are wrong)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Practical heuristic&lt;/strong&gt;: When you start scanning multiple AI threads for "what was done," you've exceeded your bandwidth. That's the signal that your loop cadence outruns your neural capacity.&lt;/p&gt;
&lt;p&gt;Ask, "What is the maximum rate at which my brain can consume and act on output?" This question defines calibration—when work aligns with your neural capacity.&lt;/p&gt;
&lt;h2 id="calibration-something-you-do-daily-not-something-you-learn-once"&gt;Calibration: Something You Do Daily, Not Something You Learn Once&lt;/h2&gt;&lt;p&gt;AI has revealed our limits: the inverted U curve has become more visible, accelerated. Our brain is the rate limiter, and rightly so! We now need to learn to brake before you hit the wall.&lt;/p&gt;
&lt;p&gt;The tools are powerful, but they don't change human neurology. No amount of prompt engineering can compress the time it takes for our brains to reason about things. If we try to go beyond our natural limits, dangerous things happen.&lt;/p&gt;
&lt;p&gt;Calibration is the new baseline practice: a discipline you maintain daily, adjusting your loop cadence to match neural throughput, closing gaps before they become exhaustions.&lt;/p&gt;
&lt;p&gt;The goal is sustainable access to the adjacent possible, rather than simply 10X-ing our output. And the goal is to do it for the long run, not just this week.&lt;/p&gt;
&lt;h3 id="what-this-looks-like-in-practice"&gt;What This Looks Like in Practice&lt;/h3&gt;&lt;p&gt;The goal is to work at the optimal effort level where output peaks. Beyond that point, additional effort produces diminishing returns and exhaustion sets in.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The default workflow:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One task at a time&lt;/strong&gt;: Start with &lt;em&gt;one&lt;/em&gt; focused task. Close the loop completely before moving to the next.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Close the loop fully&lt;/strong&gt;: Review output, make notes, then decide on the next step only when you're ready.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Eliminate scanning&lt;/strong&gt;: If you catch yourself flipping between tabs or sessions to check status, that's your signal that you've exceeded your neural throughput.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;When multiple agents must run simultaneously:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use a Kanban board&lt;/strong&gt;: A visual task queue where agents update their status automatically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agents update the board, not you&lt;/strong&gt;: The system should notify only when human intervention is needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You check the board, not the sessions&lt;/strong&gt;: Remove the need to scan through terminal windows or tool interfaces.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You synchronize to stay on the left slope of your productivity curve, where effort yields return without triggering burnout. When tools run faster than your brain can process status updates, exhaustion sets in.&lt;/p&gt;
&lt;p&gt;The rhythm shifts from "hustle harder" to &lt;em&gt;synchronize&lt;/em&gt;—matching your brain's processing speed to the tools' output rate. When the signal outruns neural processing, interference replaces insight. You tune the system to keep your brain in phase on the left slope of your productivity curve, where effort produces sustainable output.&lt;/p&gt;
</content></entry><entry><title>Undoing AI vibe-coded slop with AI</title><link href="https://ericmjl.github.io/blog/2026/3/29/undoing-ai-vibe-coded-slop-with-ai/" rel="alternate"/><updated>2026-03-29T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:e7094c8e-a057-3915-aeaf-6efb27208eec</id><content type="html">&lt;p&gt;I want to tell you about canvas-chat, a project I built with heavy AI assistance. It's a visual, non-linear chat interface where conversations are nodes on an infinite canvas — think branching, merging, and exploring topics as a directed acyclic graph.&lt;/p&gt;
&lt;p&gt;The first commit landed on December 28, 2025. By December 30, it had sessions, matrix evaluation tables, web search, node tagging, and BM25 keyword search. The AI moved &lt;em&gt;fast&lt;/em&gt;. Bugs got fixed in the next commit. Features piled in like tetris blocks stacking up.&lt;/p&gt;
&lt;p&gt;And yes, it was a mess.&lt;/p&gt;
&lt;p&gt;Here's the thing though: the mess was &lt;em&gt;recoverable&lt;/em&gt;. Not because the AI got better (it didn't; not really), but because I had battle-tested convictions on how the thing &lt;em&gt;ought&lt;/em&gt; to be architected. And those convictions came from years of shipping software, watching architectures crumble, and learning what holds up.&lt;/p&gt;
&lt;p&gt;This is the story of how we went from a jumbled 8,500-line &lt;code&gt;app.js&lt;/code&gt; to a clean plugin architecture — and why you need battle-tested convictions to make that happen.&lt;/p&gt;
&lt;h2 id="the-initial-state"&gt;The Initial State&lt;/h2&gt;&lt;p&gt;The first commit wasn't actually &lt;em&gt;bad&lt;/em&gt;. The project had clean separation from day one:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;canvas.js&lt;/code&gt; — SVG pan/zoom/rendering&lt;/li&gt;
&lt;li&gt;&lt;code&gt;graph.js&lt;/code&gt; — DAG data structure&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chat.js&lt;/code&gt; — LLM API + SSE streaming&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage.js&lt;/code&gt; — IndexedDB persistence&lt;/li&gt;
&lt;li&gt;&lt;code&gt;app.py&lt;/code&gt; — FastAPI backend&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But &lt;code&gt;app.js&lt;/code&gt; was already ~8,500 lines of everything else. Every slash command handler, every modal, every feature logic — all tangled together. Want to add a new feature? You'd grep around in that monolith, hope you found the right spot, and pray you didn't break anything.&lt;/p&gt;
&lt;p&gt;The AI could add features to this mess. It could add a &lt;code&gt;/matrix&lt;/code&gt; command in a few prompts. It could add &lt;code&gt;/search&lt;/code&gt; with Exa integration. But it couldn't see the &lt;em&gt;structure&lt;/em&gt; — the latent architecture that would make the whole thing maintainable.&lt;/p&gt;
&lt;h2 id="the-first-wave"&gt;The First Wave&lt;/h2&gt;&lt;p&gt;The refactoring started with the purest code — functions with no dependencies:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;What Got Extracted&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan 4&lt;/td&gt;
&lt;td&gt;&lt;code&gt;layout.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Overlap detection is pure math&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 5&lt;/td&gt;
&lt;td&gt;&lt;code&gt;highlight-utils.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Text selection is isolated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 7&lt;/td&gt;
&lt;td&gt;Feature modules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;flashcards.js&lt;/code&gt;, &lt;code&gt;committee.js&lt;/code&gt;, &lt;code&gt;matrix.js&lt;/code&gt;, &lt;code&gt;factcheck.js&lt;/code&gt;, &lt;code&gt;research.js&lt;/code&gt;, &lt;code&gt;code.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jan 10&lt;/td&gt;
&lt;td&gt;Core infrastructure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;undo-manager.js&lt;/code&gt;, &lt;code&gt;modal-manager.js&lt;/code&gt;, &lt;code&gt;slash-command-menu.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;This reduced &lt;code&gt;app.js&lt;/code&gt; from ~8,500 to ~5,500 lines. But these were still just &lt;em&gt;file splits&lt;/em&gt;. The code worked better, but there was no &lt;em&gt;system&lt;/em&gt; binding it together.&lt;/p&gt;
&lt;p&gt;The AI did this part reasonably well — when I asked "extract this function to a separate module," it could do it. But it never suggested "we should extract this" on its own. It needed direction.&lt;/p&gt;
&lt;h2 id="the-pivotal-moment"&gt;The Pivotal Moment&lt;/h2&gt;&lt;p&gt;This was the architectural leap. I asked the AI to create a plugin system, and it delivered — but only because I knew what a plugin system &lt;em&gt;should&lt;/em&gt; look like.&lt;/p&gt;
&lt;p&gt;We ended up with a &lt;strong&gt;three-level plugin architecture&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 1: Custom Node Types&lt;/strong&gt; — Node protocols define rendering via a &lt;code&gt;BaseNode&lt;/code&gt; class. Each node type can override &lt;code&gt;renderContent()&lt;/code&gt;, &lt;code&gt;getActions()&lt;/code&gt;, &lt;code&gt;getSummaryText()&lt;/code&gt;, and more. Registered in &lt;code&gt;node-registry.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 2: Feature Plugins&lt;/strong&gt; — Extend a &lt;code&gt;FeaturePlugin&lt;/code&gt; base class. Get &lt;code&gt;AppContext&lt;/code&gt; via dependency injection (graph, canvas, chat, storage, modalManager, streamingManager). Define slash commands via &lt;code&gt;getSlashCommands()&lt;/code&gt;. Lifecycle hooks: &lt;code&gt;onLoad()&lt;/code&gt;, &lt;code&gt;onUnload()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Level 3: Extension Hooks&lt;/strong&gt; — Subscribe to events. &lt;code&gt;CancellableEvent&lt;/code&gt; can block actions. Event names like &lt;code&gt;command:before&lt;/code&gt;, &lt;code&gt;node:created&lt;/code&gt;, &lt;code&gt;node:deleted&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The key files created:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;feature-plugin.js&lt;/code&gt; — FeaturePlugin + AppContext&lt;/li&gt;
&lt;li&gt;&lt;code&gt;feature-registry.js&lt;/code&gt; — Slash command routing with priority (BUILTIN &amp;gt; OFFICIAL &amp;gt; COMMUNITY)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;plugin-events.js&lt;/code&gt; — CanvasEvent, CancellableEvent&lt;/li&gt;
&lt;li&gt;&lt;code&gt;node-registry.js&lt;/code&gt; — Node type registration&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is where the architecture became a real system. And it only happened because I knew what I wanted.&lt;/p&gt;
&lt;h2 id="backend-pluginification-late-january-2026"&gt;Backend Pluginification (Late January 2026)&lt;/h2&gt;&lt;p&gt;The same pattern reached the Python side:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pptx_endpoints.py&lt;/code&gt; — PowerPoint handling&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ddg_endpoints.py&lt;/code&gt; — DuckDuckGo search&lt;/li&gt;
&lt;li&gt;&lt;code&gt;code_handler.py&lt;/code&gt; — Python code execution&lt;/li&gt;
&lt;li&gt;&lt;code&gt;matrix_handler.py&lt;/code&gt; — Matrix cell filling&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each follows a &lt;code&gt;register_endpoints(app)&lt;/code&gt; pattern, loaded dynamically via &lt;code&gt;importlib&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-testing-safety-net"&gt;The Testing Safety Net&lt;/h2&gt;&lt;p&gt;By late January, the plugin architecture was in place. Features were decoupled. The code was cleaner. And then GLM-4.5 started dropping curly braces.&lt;/p&gt;
&lt;p&gt;No, really. The AI would "fix" one thing and introduce a missing bracket somewhere else. Merge conflicts became minefields; features that worked yesterday stopped working today; not because of malice, but because the AI didn't understand the dependencies between modules. It was making elementary mistakes that a junior developer wouldn't make.&lt;/p&gt;
&lt;p&gt;On January 24, I added Cypress E2E tests. Out of spite, honestly. The first commit gave us &lt;code&gt;canvas_interactions.cy.js&lt;/code&gt;, &lt;code&gt;node_selection.cy.js&lt;/code&gt;, and &lt;code&gt;note_node.cy.js&lt;/code&gt; - three tests that told us whether the canvas still worked.&lt;/p&gt;
&lt;p&gt;These tests caught the regressions the AI kept introducing. More importantly, they let me verify changes faster. Instead of manually testing every feature after each AI session, I could run the test suite and know whether things still worked.&lt;/p&gt;
&lt;p&gt;The plugin architecture made the code testable. The tests caught what the AI broke.&lt;/p&gt;
&lt;h2 id="the-numbers"&gt;The Numbers&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;app.js size&lt;/th&gt;
&lt;th&gt;Modules&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial (Dec 2025)&lt;/td&gt;
&lt;td&gt;~8,500 lines&lt;/td&gt;
&lt;td&gt;5 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After feature splits&lt;/td&gt;
&lt;td&gt;~5,500 lines&lt;/td&gt;
&lt;td&gt;11 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After infrastructure&lt;/td&gt;
&lt;td&gt;~5,400 lines&lt;/td&gt;
&lt;td&gt;15 files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;After plugin migration&lt;/td&gt;
&lt;td&gt;~5,400 lines&lt;/td&gt;
&lt;td&gt;25+ files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Today&lt;/td&gt;
&lt;td&gt;~4,700 lines&lt;/td&gt;
&lt;td&gt;35+ modules&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="the-bigger-lesson"&gt;The Bigger Lesson&lt;/h2&gt;&lt;p&gt;Here's what I learned from this process:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The AI can execute architecture, but it can't design it.&lt;/strong&gt; It can split files when asked. It can implement a plugin system from a spec. But it won't look at a 8,500-line &lt;code&gt;app.js&lt;/code&gt; and say "this should be a plugin system."&lt;/p&gt;
&lt;p&gt;That vision; that &lt;em&gt;opinion&lt;/em&gt;; comes from somewhere else. It comes from:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Seeing architectures fail&lt;/strong&gt; - Knowing the pain of tangled code, merged conflicts, feature creep&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Seeing architectures succeed&lt;/strong&gt; - Knowing what maintainable code feels like after years of shipping&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reading, studying, internalizing&lt;/strong&gt; - Design patterns, architectural styles, tradeoffs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Making mistakes&lt;/strong&gt; - Building the wrong abstraction once so you recognize it next time&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I didn't arrive at "we need a three-level plugin architecture" out of nowhere. It came from discussing tradeoffs with the AI; asking "what if we did it this way?" and "what are the tradeoffs of that approach?"; and applying my best judgment to the options. The AI could explain the pros and cons of different approaches, but I had to pick which tradeoffs I was willing to accept.&lt;/p&gt;
&lt;p&gt;The AI didn't teach me this. &lt;em&gt;Experience&lt;/em&gt; taught me this.&lt;/p&gt;
&lt;h2 id="what-this-means-for-the-future"&gt;What This Means for the Future&lt;/h2&gt;&lt;p&gt;Here's where it gets interesting. Because we built this modular foundation, I can now swap out the rendering layer. The canvas is currently raw SVG — and I want to move to Svelte Flow. The plugin system I built makes this possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Features don't depend on &lt;code&gt;app.js&lt;/code&gt; internals; they use &lt;code&gt;AppContext&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Canvas is isolated in &lt;code&gt;canvas.js&lt;/code&gt;; swapping to Svelte Flow means replacing that layer&lt;/li&gt;
&lt;li&gt;Node protocols define behavior; Svelte Flow nodes can use the same protocol pattern&lt;/li&gt;
&lt;li&gt;Event system is framework-agnostic&lt;/li&gt;
&lt;li&gt;Dependency injection provides graph, canvas, chat, storage; these can be re-provided to Svelte Flow components&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The abstraction layer we built; FeaturePlugin + AppContext + EventSystem; separates &lt;em&gt;what&lt;/em&gt; features do from &lt;em&gt;how&lt;/em&gt; they're rendered. That's what makes Svelte Flow viable as a drop-in replacement.&lt;/p&gt;
&lt;h2 id="closing-thoughts"&gt;Closing Thoughts&lt;/h2&gt;&lt;p&gt;You can undo AI vibe-coded slop. It's possible. But it requires &lt;em&gt;you&lt;/em&gt; to have battle-tested convictions on how the thing ought to be.&lt;/p&gt;
&lt;p&gt;The AI is an incredible executor. It can refactor, extract, implement. But the vision? That stays human. And that vision comes from battle-tested experience; from having seen enough codebases to know what works and what collapses under its own weight.&lt;/p&gt;
&lt;p&gt;So if you're working with AI coding assistants: don't expect them to architect for you. Tell them what to build. Give them the structure. Then let them do the implementation.&lt;/p&gt;
&lt;p&gt;That's how you get from a jumbled mess to something you can actually maintain.&lt;/p&gt;
</content></entry><entry><title>Creative mentorship strategies for career growth in challenging times</title><link href="https://ericmjl.github.io/blog/2026/3/25/creative-mentorship-strategies-for-career-growth-in-challenging-times/" rel="alternate"/><updated>2026-03-25T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:9e5abdb7-d7d1-32b6-9593-332abe1cdbb5</id><content type="html">&lt;h2 id="the-problem-with-lean-times"&gt;The problem with lean times&lt;/h2&gt;&lt;p&gt;When the economy tightens, formal development opportunities are usually the first things to go. Co-ops get paused, training budgets shrink, and headcount freezes make it harder to bring in fresh talent. But the need to develop mentorship, coaching, and leadership skills doesn't disappear just because the budget did.&lt;/p&gt;
&lt;p&gt;So the question becomes: how do you get creative? How do you find opportunities to grow as a mentor and leader without requiring the company to spend additional money?&lt;/p&gt;
&lt;h2 id="you-already-have-something-to-offer"&gt;You already have something to offer&lt;/h2&gt;&lt;p&gt;The answer is closer than you think. Even when budgets are frozen, you still have three things worth sharing: your judgment, your skills, and your network.&lt;/p&gt;
&lt;p&gt;Your judgment is what experience actually gives you: not just knowing things, but knowing which approach to take, which tradeoffs matter, and when to push versus when to hold back. Your skills are the technical foundation that lets you coach and mentor beyond your own team, helping others onboard to the tools and practices you work with. And your network is the set of connections you can activate to create opportunities for others, whether that means knowing the right organizer, connecting a speaker to an audience, or simply inviting people into the same room.&lt;/p&gt;
&lt;p&gt;The people who want to learn from you are already in your neighborhood. If you are doing an excellent job, you will find individuals who are eager to understand how you achieve your results. That is where your opportunity lies.&lt;/p&gt;
&lt;h2 id="five-strategies-that-have-worked-for-me"&gt;Five strategies that have worked for me&lt;/h2&gt;&lt;p&gt;I want to share some concrete strategies that have worked at the two companies I have been with, Novartis and Moderna. Some of these I have actively advocated for. My intent is not to boast but to provide pragmatic suggestions based on my own experiences.&lt;/p&gt;
&lt;h3 id="coach-others-one-on-one"&gt;Coach others one-on-one&lt;/h3&gt;&lt;p&gt;Coaching others is a great way to build your reputation within the organization. When you teach someone how to accomplish a task effectively, you become valuable to them. More importantly, you demonstrate your value to a broader audience. Within an organization, you want a group of people who find your skills genuinely worthwhile.&lt;/p&gt;
&lt;h3 id="present-at-internal-guilds-and-birds-of-a-feather-events"&gt;Present at internal guilds and "birds of a feather" events&lt;/h3&gt;&lt;p&gt;At Moderna's Digital organization, we have "Guilds", the Data Science Guild being one, with three meetings per month for the guild. When I was at Novartis' Research org, we had the Computational Research Community. Both served as outlets for talks and annual gatherings. The key is to be in a position where you can give a talk about something valuable to others, and they would willingly spend an hour listening to you. If that happens, you have created another mentorship opportunity for yourself.&lt;/p&gt;
&lt;h3 id="organize-communities-of-practice"&gt;Organize communities of practice&lt;/h3&gt;&lt;p&gt;I have seen this happen when someone builds a tool that others use and then creates a community around that tool. It can be as simple as a Microsoft Teams group chat. You don't need anything more sophisticated than that. Just gather people who use the tool and facilitate discussions. Being a leader in that group chat is a real way to hone your leadership skills across the organization. My colleague &lt;a href="https://www.linkedin.com/in/albert-lam/"&gt;Albert Lam&lt;/a&gt; built a significant portion of the Python packages that are used by LLM builders, and put together communities of practice around that precisely in the form of MS Teams group chats.&lt;/p&gt;
&lt;p&gt;Another example is the community of practice around documentation - primarily expressed as the internal &lt;a href="../../../../2024/6/30/two-years-of-docathons-insights-and-lessons-learned/"&gt;docathons&lt;/a&gt; that we run. My teammate &lt;a href="https://jackievaleri.github.io/"&gt;Jackie Valeri&lt;/a&gt;, as well as two other colleagues &lt;a href="https://www.linkedin.com/in/simreen-kaur/"&gt;Simreen Kaur&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/saakshisdonthi/"&gt;Saakshi Shamanth Donthi&lt;/a&gt; help coordinate and organize the logistics while also being point contacts for other folks participating in the docathon.&lt;/p&gt;
&lt;h3 id="host-informal-coffee-hours"&gt;Host informal coffee hours&lt;/h3&gt;&lt;p&gt;My teammate, &lt;a href="https://mfaits.github.io/"&gt;Michelle Faits&lt;/a&gt;, took the initiative to host coffee hours within the company. These serve as informal outlets for people to present their work, and they are great because they are relaxed and authentic. As her manager, I try to find speakers to contribute and support her efforts. Kudos to her for initiating this.&lt;/p&gt;
&lt;h3 id="host-or-support-external-meetups"&gt;Host or support external meetups&lt;/h3&gt;&lt;p&gt;We also host the PyData Boston/Cambridge monthly meetup at Moderna. Not every month is held at our location, but since I know the organizer &lt;a href="https://benbatorsky.com/"&gt;Ben Batorsky&lt;/a&gt;, back in 2025, I offered my time to book a room; we simply provide the space. More recently, Jackie has taken the lead in this. By taking charge and inviting others to network, we create opportunities for people to grow in their careers without any budget requirement.&lt;/p&gt;
&lt;h2 id="advice-for-managers"&gt;Advice for managers&lt;/h2&gt;&lt;p&gt;If you are a manager, recognize that there will be projects and efforts that need leadership beyond individual contributions. Helping others improve their skills and providing them with opportunities to lead is part of the job, especially when formal avenues are limited.&lt;/p&gt;
&lt;p&gt;Make sure you are aware of these kinds of initiatives among your reports, and ensure they don't conflict with core responsibilities. When someone can demonstrate that they manage these additional activities while maintaining their primary work, that forms a strong case for their expanded capabilities.&lt;/p&gt;
&lt;p&gt;We should expand our imagination beyond just climbing the career ladder, attaining higher status, or managing other people, which sometimes unfortunately spills over into controlling others at work. Growth comes in many forms, and we can find meaningful ways to develop without waiting for formal promotions or titles.&lt;/p&gt;
&lt;h2 id="the-core-principles"&gt;The core principles&lt;/h2&gt;&lt;p&gt;Mentoring is about sharing your judgment, providing opportunities for others to share theirs, facilitating networking, and helping others grow. If we limit our understanding of growth and development to a narrow definition (only formal programs, only budgeted activities, only additional formal assignments), we constrain our imagination as managers.&lt;/p&gt;
&lt;p&gt;Don't be confined to a singular vision of what it means to be a good leader or manager. Embrace the diverse talents and varying stages of abilities within your team. Encourage your team members to step outside their comfort zones and provide them with opportunities to grow.&lt;/p&gt;
&lt;p&gt;While good, you do not necessarily need an internal university to foster a learning culture; your environment is where you learn and grow. We have more autonomy and agency than we might realize!&lt;/p&gt;
</content></entry><entry><title>Closing air gaps</title><link href="https://ericmjl.github.io/blog/2026/3/15/closing-air-gaps/" rel="alternate"/><updated>2026-03-15T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:739d4d76-ce27-3a06-a50b-cffe98d01bbb</id><content type="html">&lt;p&gt;I owe this term to my colleague &lt;a href="https://www.linkedin.com/in/wenhao-liu-1b85177/"&gt;Wenhao Liu&lt;/a&gt;. He was the first one I saw at work who clearly articulated about air gaps and how they relate to building agents for work.&lt;/p&gt;
&lt;p&gt;So what exactly is an air gap? It is any point in a business or scientific process where a human has to intervene and perform manual work before a digital system can continue. The system cannot go end to end on its own; the human is the bridge.&lt;/p&gt;
&lt;p&gt;Air gaps are everywhere. Here are a few examples: A laboratory machine exports a file to a local hard disk. A human copies that file and pastes it into an S3 bucket. That handoff is an air gap. The system stops at the hard disk and waits for a person.&lt;/p&gt;
&lt;p&gt;In wet lab science, air gaps take physical form. A scientist designs an experiment, walks into the lab, and executes it by hand. In this state, the company/team/org has an air gap in its scientific process. No robotic system can take over from design to execution.&lt;/p&gt;
&lt;p&gt;Both of these examples share a common pattern. The definition I have settled on is this: an air gap is any place where rote manual work is performed by a human that could have been done by a computer. (Robots are computers with sensors and actuators for the physical world.)&lt;/p&gt;
&lt;h2 id="why-air-gaps-matter"&gt;Why air gaps matter&lt;/h2&gt;&lt;p&gt;Think of your processes as pipes. Air gaps are bubbles trapped in those pipes. They slow the flow. They disrupt continuity. They introduce delays and errors.&lt;/p&gt;
&lt;p&gt;The costs compound over time. A five-minute manual handoff, repeated daily across a team of twenty, adds up to real hours. A week-long delay because someone was on vacation and could not move the file. A transcription error because a human typed a number wrong. A lost opportunity because the data sat in a local folder instead of flowing into the analysis pipeline.&lt;/p&gt;
&lt;p&gt;Air gaps also create cognitive overhead. Every time a human has to remember to perform a manual step, that is mental bandwidth not spent on creative work. The air gap is a tax on attention.&lt;/p&gt;
&lt;p&gt;Now, a clarification. The goal here is not to eliminate humans from everything. The goal is to eliminate humans from the rote and routine. Creative work, judgment, and decision making stay with us. Copying files, transferring plates, and typing data into forms do not.&lt;/p&gt;
&lt;h2 id="how-to-find-air-gaps"&gt;How to find air gaps&lt;/h2&gt;&lt;p&gt;You cannot close an air gap until you see it. And you cannot see it until you sit down and map out exactly how your process works. In other words, &lt;strong&gt;process mapping&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This mapping exercise is the unsexy work that precedes automation. Most teams skip it. They jump to solutions before understanding the problem. But you need the map.&lt;/p&gt;
&lt;p&gt;Here is how to do it.&lt;/p&gt;
&lt;p&gt;Pick one process. It could be a data pipeline, a lab workflow, or a business approval chain. Walk through it step by step. Ask these questions at each step:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Where is a human manually copying and pasting?&lt;/li&gt;
&lt;li&gt;Where is a human manually entering data?&lt;/li&gt;
&lt;li&gt;Where is a human dragging and dropping files?&lt;/li&gt;
&lt;li&gt;Where is a human making a decision that follows a fixed rule?&lt;/li&gt;
&lt;li&gt;Where is a human waiting for another human to take action?&lt;/li&gt;
&lt;li&gt;Where is a human physically moving something from one place to another?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each answer points to a potential air gap.&lt;/p&gt;
&lt;p&gt;Write it down. Draw it out. Make the process visible. Once the map exists, the air gaps reveal themselves. The next step is prioritization. Which air gaps cause the most pain? Which ones are easiest to close? Start there.&lt;/p&gt;
&lt;h2 id="air-gaps-in-the-wild"&gt;Air gaps in the wild&lt;/h2&gt;&lt;p&gt;Enough abstraction. Let me show you what air gaps look like in practice, from my own work and from the broader landscape.&lt;/p&gt;
&lt;h3 id="file-schlepping-in-the-lab"&gt;File schlepping in the lab&lt;/h3&gt;&lt;p&gt;A sequencing machine finishes a run. It writes the data to a local drive. A technician notices the run is complete, navigates to the folder, selects the files, copies them, navigates to the shared storage system, and pastes. Minutes pass. Sometimes hours, if the technician is busy.&lt;/p&gt;
&lt;p&gt;This is an air gap. The machine knows when the run finishes. The machine has network access. The destination storage has an API. Automation can close this gap with a simple script that watches for new files and uploads them.&lt;/p&gt;
&lt;p&gt;The fix is not technically difficult; it is, conceptually, a &lt;code&gt;cron&lt;/code&gt; job with &lt;code&gt;rsync&lt;/code&gt;. What makes it hard is that the air gap is invisible until someone maps the process and asks why a human is doing this work.&lt;/p&gt;
&lt;h3 id="github-activity-tracking"&gt;GitHub activity tracking&lt;/h3&gt;&lt;p&gt;I used to manually check GitHub to track my daily work. I would open my profile page, scroll through recent commits, open the pull requests tab, check which ones I had opened or reviewed, and then type notes into a document. This took maybe ten minutes per day.&lt;/p&gt;
&lt;p&gt;Then I remembered the GitHub CLI exists. I also remembered that coding agents can run CLI commands.&lt;/p&gt;
&lt;p&gt;I built a skill that pulls four categories of activity automatically: my opened pull requests, pull requests I reviewed or commented on, my commits, and issues I created. The agent runs this skill as part of my daily sign-off routine. The air gap closed.&lt;/p&gt;
&lt;p&gt;The time savings are modest. But the mental overhead vanished. I no longer need to remember to check GitHub. The information flows to me.&lt;/p&gt;
&lt;h3 id="autonomous-laboratories"&gt;Autonomous laboratories&lt;/h3&gt;&lt;p&gt;The autonomous lab, sometimes called a lights-out lab, is the ultimate expression of closing air gaps. The vision is a laboratory that runs itself: experiments are designed, executed, analyzed, and iterated without human intervention.&lt;/p&gt;
&lt;p&gt;In practice, autonomous labs are full of micro air gaps. Each one must be identified and closed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Plate transfers.&lt;/strong&gt; A protocol requires moving a plate from one instrument to another. Does a human do this? If so, that is an air gap. Robotic arms and conveyor systems can close it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Master reagent prep.&lt;/strong&gt; Someone mixes buffers and reagents by hand at the start of each week. Could a liquid handling robot do this instead? Probably. That is an air gap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;File movement.&lt;/strong&gt; Instruments write data locally. Humans move data to shared storage. This is the file schlepping problem again, repeated across every machine in the lab.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Standardized analyses.&lt;/strong&gt; As a data scientist, this one is close to my heart. Most labs have a set of standard analyses they run on every dataset. Quality control plots, basic statistics, alignment checks. A human opens a notebook, loads the data, runs the cells, and exports results.&lt;/p&gt;
&lt;p&gt;This is an air gap. Standardized analyses can be automated. They can also be made adaptable. An LLM-powered coding agent can take a standard analysis template and adjust parameters within a confined range of design choices. The human specifies the intent. The agent handles the implementation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Closing the loop.&lt;/strong&gt; The ultimate air gap in scientific research is the gap between analysis and experiment design. A human looks at results, draws conclusions, and designs the next experiment. What if the results could flow back into experiment design automatically? What if an agent could propose the next experiment based on what the data showed?&lt;/p&gt;
&lt;p&gt;This is the direction autonomous labs are moving. But getting there requires closing every air gap along the chain.&lt;/p&gt;
&lt;h2 id="the-blockers-imagination-and-skill"&gt;The blockers: imagination and skill&lt;/h2&gt;&lt;p&gt;I wrote about this in a &lt;a href="https://ericmjl.github.io/blog/2026/3/6/mastering-personal-knowledge-management-with-obsidian-and-ai/"&gt;previous post&lt;/a&gt;. The biggest blockers to closing air gaps are technical skill and imagination.&lt;/p&gt;
&lt;h3 id="imagination"&gt;Imagination&lt;/h3&gt;&lt;p&gt;If you cannot imagine a future state where your tedious work is performed by a coding agent, you will not see the possibility. The air gap remains invisible.&lt;/p&gt;
&lt;p&gt;This is a failure of imagination, not a failure of technology. The tools exist. The APIs exist. The agents exist. What is missing is the mental leap from "this is how we have always done it" to "this is how we could do it."&lt;/p&gt;
&lt;p&gt;Imagination grows from exposure. The more you see what is possible, the more you can imagine for your own work. Watch what other teams are doing. Read about automation in adjacent fields. Talk to people who have closed similar gaps.&lt;/p&gt;
&lt;h3 id="skill"&gt;Skill&lt;/h3&gt;&lt;p&gt;Imagination alone is not enough. You also need the skill to build the automation.&lt;/p&gt;
&lt;p&gt;That skill, at some level, means knowing how to program. It means understanding APIs, scripting, and how systems talk to each other. It means knowing that cron jobs and web hooks can be configured.&lt;/p&gt;
&lt;p&gt;The good news is that the barrier to entry is lower than ever. Coding agents can help you write the code. The skill you need is not deep software engineering. It is enough programming literacy to describe what you want and recognize whether the output is correct.&lt;/p&gt;
&lt;h3 id="programmatic-access"&gt;Programmatic access&lt;/h3&gt;&lt;p&gt;Sometimes the blocker is not you. Sometimes it is the system.&lt;/p&gt;
&lt;p&gt;Some organizations block programmatic access to their tools. It may be that a SaaS application has no API, or the API is disabled for security reasons, or that a legacy database has no query interface, only a web portal. A legacy laboratory information management system may require a human to click through screens instead of secured APIs.&lt;/p&gt;
&lt;p&gt;These are also air gaps. The system itself prevents automation.&lt;/p&gt;
&lt;p&gt;If the blocker is cybersecurity concerns, push for scoped, tracked access. You do not need full API permissions to close a specific air gap. You need enough access to move the data that belongs in your pipeline. That access can be limited, logged, and auditable.&lt;/p&gt;
&lt;p&gt;If the system truly has no programmatic interface, browser or desktop automation agents can close the gap. A headless browser can log in, navigate, click, and extract. It is slower and more fragile than an API, but it works.&lt;/p&gt;
&lt;h2 id="closing-air-gaps-with-agents"&gt;Closing air gaps with agents&lt;/h2&gt;&lt;p&gt;But here is the good news. The rise of coding agents changes the calculus for closing air gaps.&lt;/p&gt;
&lt;p&gt;Before, you needed a software engineer to write the automation script. Now, you can describe what you want in plain language and let the agent write the code.&lt;/p&gt;
&lt;p&gt;This does not mean you can ignore technical literacy. You still need to verify the output, debug when things break, and understand enough to specify the problem clearly. But the implementation barrier is lower.&lt;/p&gt;
&lt;p&gt;Browser agents extend this further. If a system has no API, a browser agent can act as the interface. Log in, click the buttons, extract the data, and feed it into your pipeline.&lt;/p&gt;
&lt;p&gt;The key insight is that agents are not a replacement for mapping your processes. They are a tool for closing the air gaps you have already identified. The mapping still matters. The imagination still matters. The skill to recognize whether the agent's output is correct still matters.&lt;/p&gt;
&lt;p&gt;What changes is the speed of iteration. You can try closing an air gap in an afternoon instead of a sprint. You can experiment with different approaches quickly. The feedback loop tightens.&lt;/p&gt;
&lt;p&gt;Here is the right question to ask when you find yourself being asked to do something repeatedly: "Can I remove this bottleneck for you? Is there a way I can make it so that you never have to ask me this again?" The goal is to build systems that use agents to not need human intervention as much as possible, not to create systems that depend on humans more and more. As one person put it, "the more I have asked myself that question, the more capable he has become." (&lt;a href="https://www.youtube.com/watch?v=nSBKCZQkmYw"&gt;source&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="the-principle"&gt;The principle&lt;/h2&gt;&lt;p&gt;The guiding principle is simple. To butcher the Biblical phrase, "Give unto robots what belongs to robots, and have humans do what humans can do." Or to paraphrase another person, use robots for the dull, dirty, and dangerous work.&lt;/p&gt;
&lt;p&gt;Keep the human in the loop for creative, judgment-heavy work. The rote and routine should flow through pipes without bubbles.&lt;/p&gt;
&lt;p&gt;Start mapping. Find the air gaps. Close them one by one. The compounding effect over time is enormous once micro-efficiencies become part of your work.&lt;/p&gt;
&lt;p&gt;What looks like a small efficiency gain today becomes a transformed process tomorrow. The lab that closed its file schlepping air gaps is one step closer to autonomous operation. The team that automated their daily reporting has mental bandwidth for harder problems.&lt;/p&gt;
&lt;p&gt;Close every air gap you can see!&lt;/p&gt;
</content></entry><entry><title>Agent skills are also human skills</title><link href="https://ericmjl.github.io/blog/2026/3/14/agent-skills-are-also-human-skills/" rel="alternate"/><updated>2026-03-14T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:3258471c-f953-3f42-92b1-712fa77e264b</id><content type="html">&lt;p&gt;Agent skills are great, but I've been thinking about this... skills alone aren't enough.&lt;/p&gt;
&lt;p&gt;I've been thinking about this while developing and using agent skills at home and at work. There's a distinction I've started to draw between two types of skills. Tool-specific skills document how to work with a particular tool or package. Those are fine, but really, pointing an agent at &lt;code&gt;llms.txt&lt;/code&gt; often works just as well. The more interesting category is workflow-specific skills, things that encode how you actually work, that string together multiple tools to &lt;strong&gt;get a job done&lt;/strong&gt; (Christensen).&lt;/p&gt;
&lt;p&gt;Workflow-specific skills are what I want to talk about here.&lt;/p&gt;
&lt;h2 id="a-concrete-example"&gt;A concrete example&lt;/h2&gt;&lt;p&gt;My daily sign-off skill, which I use at work, is a case in point. I use it to wrap up my day. When I sign off, I need two things: my meeting notes (which I paste into Obsidian throughout the day) and my GitHub activity (commits, PRs, comments, reviews). The skill handles the GitHub part by querying the GitHub CLI and formatting everything into my daily bullets template.&lt;/p&gt;
&lt;p&gt;But here's where it gets opinionated. My skill assumes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have the GitHub CLI installed&lt;/li&gt;
&lt;li&gt;You do PRs as part of your work (not all technical managers do)&lt;/li&gt;
&lt;li&gt;You write into a monthly file as your bullet journal, rather than having a single note per day.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is opinionated. I don't have a single note per day. Instead, each month contains my collection of daily bullets. The motivation here is a line from the Zen of Python -- "flat is better than nested". On March 26, I have entries for that day inside the March file, rather than have a reference from the March file to March 26. This might not reflect your own preferences; you might prefer one note per day, or use a different structure entirely. But this is what my skill expects, and it's baked into how the skill works.&lt;/p&gt;
&lt;p&gt;If you want to use my daily sign-off skill, you're not just adopting the skill. You're adopting my way of working. You're inheriting my file structure, my tool preferences, my mental model for organizing information. The skill comes with implicit assumptions about how you work, what tools you use, and what your environment looks like.&lt;/p&gt;
&lt;h2 id="a-second-example-cutting-deeper"&gt;A second example, cutting deeper&lt;/h2&gt;&lt;p&gt;The daily sign-off is mostly about tool and structure preferences. But some skills go further — they encode a &lt;em&gt;philosophy&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;My scientific EDA skill is a good example. On the surface it looks like a set of technical rules: use &lt;code&gt;uv&lt;/code&gt; with PEP723 inline scripts, save plots as WebP (not PNG), organize each analysis session into a timestamped folder, keep an append-only &lt;code&gt;journal.md&lt;/code&gt;. But look at what those rules actually encode:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One step at a time, ask "why" before executing&lt;/strong&gt; — this isn't a technical constraint. It reflects a skepticism of agents that run ahead of the analyst. I believe good exploratory analysis is a dialogue, not a sprint.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capture the research question before touching the data&lt;/strong&gt; — this reflects a conviction that context shapes what you should even be looking for. Data without a question is just noise.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Append-only journal&lt;/strong&gt; — this reflects a belief that good science is narrated, not just executed. The journal isn't a log file; it's a record of reasoning.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebP over PNG&lt;/strong&gt; — a small but deliberate aesthetic and practical stance on file hygiene.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;uv + PEP723&lt;/strong&gt; — a specific bet on the Python toolchain that not everyone has made.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of these are neutral defaults. Each one is a choice that reflects how I think scientific work should be done. If you use my EDA skill but don't share that underlying philosophy, you'll find yourself fighting it. The one-step-at-a-time rule will feel like friction. The journaling requirement will feel like overhead. The skill isn't broken — it's just mine.&lt;/p&gt;
&lt;p&gt;This is a different kind of assumption from the daily sign-off. There, you're inheriting my tools and file layout. Here, you're inheriting my epistemology. That's harder to see, harder to document, and harder to transfer.&lt;/p&gt;
&lt;h2 id="what-this-means"&gt;What this means&lt;/h2&gt;&lt;p&gt;I call this &lt;em&gt;procedural context&lt;/em&gt;. A workflow-specific agent skill is more than documentation for the coding agent. It also implicitly encodes a person's systems and structures for working. Without documenting the procedural context, the skill can only be half-useful for another person.&lt;/p&gt;
&lt;p&gt;The two examples above hint at different layers of procedural context. There are at least three:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Tool dependencies&lt;/strong&gt; — what software needs to be installed (GitHub CLI, &lt;code&gt;uv&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organizational preferences&lt;/strong&gt; — how you structure files, folders, and notes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Epistemic preferences&lt;/strong&gt; — how you believe the work should actually proceed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The third layer is the most invisible. It's also the most important, and the hardest to transfer. You can install a CLI tool in five minutes. Adopting someone else's philosophy of scientific analysis is a different ask entirely.&lt;/p&gt;
&lt;p&gt;Someone on Twitter put it well (I wish I could remember who, so I won't take credit): with agent skills, we finally found a way to get coders to write documentation. We'll document how we work if it means we can delegate that work to some{one/thing} else!&lt;/p&gt;
&lt;p&gt;At the end of the day, agent skills are just automation and documentation. We're automating away the minutiae, and I love that. But if your skill describes a workflow, you need to document the assumptions too. What are the dependencies? What tools need to be installed? What mental structures does the person need? What does the user need to know to verify the output is correct?&lt;/p&gt;
&lt;p&gt;Without that context, you can't evaluate whether the coding agent used the skill correctly -- and verification matters! You need to know what to look for when an LLM does work on your behalf.&lt;/p&gt;
&lt;h2 id="the-takeaway"&gt;The takeaway&lt;/h2&gt;&lt;p&gt;Agent skills implicitly involve human skills. If that's true, then agent skills are also for humans. They're not merely instructions for an agent. They're documentation of how someone accomplishes a job, with all the prerequisites and context needed to reproduce it.&lt;/p&gt;
&lt;p&gt;So when you write a workflow skill, think about the other people who might use it. Ask the skill-creator skill to include the dependencies, explain the environment, and describe what success looks like. The skill alone isn't enough. We have to teach the next person how to use it too.&lt;/p&gt;
</content></entry><entry><title>My weekend experiment making PyMC installable in a WASM environment</title><link href="https://ericmjl.github.io/blog/2026/3/8/my-weekend-experiment-pymc-wasm/" rel="alternate"/><updated>2026-03-08T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:bec96de2-ebd1-3052-a10c-136d1b83cc99</id><content type="html">&lt;p&gt;This past weekend, I found myself revisiting a blog post from PyMC Labs titled &lt;a href="https://www.pymc-labs.com/blog-posts/pymc-in-browser"&gt;"Running PyMC in the Browser with PyScript"&lt;/a&gt;. Published in 2022, it demonstrated something magical: running full Bayesian inference with PyMC entirely in the browser—no server, no installation, no data leaving your device. Users could define models, run NUTS sampling, and visualize posteriors, all client-side.&lt;/p&gt;
&lt;p&gt;I was excited to try it out. But when I attempted to run the examples, I discovered they no longer worked. The Python package ecosystem had evolved, dependencies had shifted, and the Pyodide environment had changed. What was once a breakthrough demo had quietly broken.&lt;/p&gt;
&lt;p&gt;So I did what any curious engineer would do on a weekend: I dove down the rabbit hole to figure out how to make it work again.&lt;/p&gt;
&lt;h2 id="the-core-challenge-getting-pytensor-to-build-for-webassembly"&gt;The core challenge: getting PyTensor to build for WebAssembly&lt;/h2&gt;&lt;p&gt;PyMC depends on PyTensor, its computational backend. PyTensor is where the heavy lifting happens: it compiles mathematical expressions into optimized code (usually C or JAX) and executes them efficiently. To run PyMC in a browser via Pyodide, I first needed to make PyTensor installable in a WebAssembly environment.&lt;/p&gt;
&lt;p&gt;This wasn't just a matter of &lt;code&gt;pip install&lt;/code&gt;. PyTensor contains C and Cython extensions that must be compiled for the target platform. For WebAssembly, that means using Emscripten and the Pyodide build tooling.&lt;/p&gt;
&lt;h2 id="the-code-changes-what-i-modified-in-pytensor"&gt;The code changes: what I modified in PyTensor&lt;/h2&gt;&lt;p&gt;Working on my fork of PyTensor (&lt;a href="https://github.com/ericmjl/pytensor"&gt;ericmjl/pytensor&lt;/a&gt;), I made targeted modifications to enable WASM builds. Here's the complete diff:&lt;/p&gt;
&lt;h3 id="change-1-making-numba-optional-on-webassembly"&gt;Change 1: making Numba optional on WebAssembly&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gd"&gt;-    &amp;quot;numba&amp;gt;0.57,&amp;lt;1&amp;quot;,&lt;/span&gt;
&lt;span class="gi"&gt;+    &amp;quot;numba&amp;gt;0.57,&amp;lt;1; platform_machine != &amp;#39;wasm32&amp;#39; and sys_platform != &amp;#39;emscripten&amp;#39;&amp;quot;,&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This single line change is the critical enabler. Numba, PyTensor's JIT compiler for numerical code, is not available in WebAssembly environments. There's no way to install it—it simply doesn't exist for this platform.&lt;/p&gt;
&lt;p&gt;The fix uses PEP 508 environment markers to make Numba a conditional dependency:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;platform_machine != 'wasm32'&lt;/code&gt; excludes WASM architectures&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys_platform != 'emscripten'&lt;/code&gt; adds an extra safety check for Emscripten-based builds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without this change, attempting to install PyTensor in Pyodide would fail immediately with a dependency resolution error. Pyodide would try to find a Numba wheel for WASM, fail, and abort the entire installation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The tradeoff, however, is that PyTensor loses its JIT compilation capabilities on WASM.&lt;/strong&gt; Operations that would be compiled to optimized native code fall back to pure Python execution. This means slower performance, and critically, PyMC's NUTS sampler won't work.&lt;/p&gt;
&lt;h3 id="change-2-adding-pixi-development-environment-configuration"&gt;Change 2: adding Pixi development environment configuration&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;pyproject.toml&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I added a complete Pixi workspace configuration to &lt;code&gt;pyproject.toml&lt;/code&gt;. This provides a reproducible development environment and includes the tooling needed to build WASM wheels:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# -----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="c1"&gt;# Pixi (pixi.prefix.dev): development environment from environment.yml&lt;/span&gt;
&lt;span class="c1"&gt;# Use: pixi install &amp;amp;&amp;amp; pixi run pytest   or   pixi shell&lt;/span&gt;
&lt;span class="c1"&gt;# -----------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="k"&gt;[tool.pixi.workspace]&lt;/span&gt;
&lt;span class="n"&gt;channels&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;conda-forge&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;platforms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;linux-64&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;osx-64&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;osx-arm64&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;win-64&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.pypi-dependencies]&lt;/span&gt;
&lt;span class="n"&gt;pytensor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;types-setuptools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pyodide-build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=0.29.2&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=3.11,&amp;lt;3.14&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;compilers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=2.0.0&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;scipy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=1,&amp;lt;2&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;filelock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=3.15&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;etuples&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;logical-unification&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;miniKanren&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;cons&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pydeprecate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;numba&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=0.57&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;coveralls&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;diff-cover&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;mypy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest-cov&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest-xdist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest-benchmark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest-mock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pytest-sphinx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;sphinx&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=5.1.0,&amp;lt;6&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;sphinx_rtd_theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pygments&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pydot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ipython&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pymc-sphinx-theme&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;sphinx-design&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;myst-nb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;matplotlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;watermark&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;ruff&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pandas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pre-commit&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;packaging&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;cython&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;graphviz&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.target.linux-64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;mkl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;mkl-service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*mkl&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.target.win-64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;mkl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;mkl-service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*mkl&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.target.osx-64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*accelerate&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.target.osx-arm64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*accelerate&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.tasks]&lt;/span&gt;
&lt;span class="n"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pytest&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;lint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ruff check .&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ruff format .&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;docs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;python -m sphinx -b html ./doc ./html&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;wheel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;python -m build --wheel&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;sdist&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;python -m build --sdist&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;wheel-wasm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pyodide build&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here are the key design decisions in this configuration:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python version pinning:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=3.11,&amp;lt;3.14&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pyodide only supports up to Python 3.13. Without this constraint, the environment might resolve to Python 3.14+, causing the WASM build to fail with: &lt;code&gt;ValueError: Python version 3.14 is not yet supported.&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PyPI dependencies for building:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[tool.pixi.pypi-dependencies]&lt;/span&gt;
&lt;span class="n"&gt;pytensor&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;editable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;types-setuptools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;pyodide-build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;gt;=0.29.2&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This installs PyTensor in editable mode for development, includes type stubs for mypy, and adds both &lt;code&gt;build&lt;/code&gt; (standard wheel building) and &lt;code&gt;pyodide-build&lt;/code&gt; (WASM wheel building).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Platform-specific BLAS:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;[tool.pixi.target.linux-64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;mkl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;mkl-service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*mkl&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;[tool.pixi.target.osx-arm64.dependencies]&lt;/span&gt;
&lt;span class="n"&gt;libblas&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;*accelerate&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Different platforms use different BLAS implementations. Linux and Windows use Intel MKL, while macOS uses Apple's Accelerate framework. These ensure the correct linear algebra library is installed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Build task:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;wheel-wasm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pyodide build&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This task runs &lt;code&gt;pyodide build&lt;/code&gt;, which compiles PyTensor for WebAssembly using Emscripten.&lt;/p&gt;
&lt;h3 id="change-3-documenting-the-wasm-build-process"&gt;Change 3: documenting the WASM build process&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;File:&lt;/strong&gt; &lt;code&gt;doc/dev_start_guide.rst&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I added documentation explaining how to build WASM wheels:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gh"&gt;Building a WebAssembly (Pyodide) wheel&lt;/span&gt;
&lt;span class="gh"&gt;-------------------------------------&lt;/span&gt;

To build a wheel targeting WebAssembly for use with &lt;span class="s"&gt;`Pyodide &lt;/span&gt;&lt;span class="si"&gt;&amp;lt;https://pyodide.org/&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;`_&lt;/span&gt; (e.g. for the browser or JupyterLite), use the Pyodide build tooling. This produces a wheel in &lt;span class="s"&gt;``dist/``&lt;/span&gt; with a name like &lt;span class="s"&gt;``*-cpXXX-cpXXX-pyodide_*_wasm32.whl``&lt;/span&gt;.

&lt;span class="gs"&gt;**One-time setup: Emscripten**&lt;/span&gt;

&lt;span class="m"&gt;1.&lt;/span&gt; Install &lt;span class="nv"&gt;`pyodide-build`&lt;/span&gt; (included in the Pixi dev env, or &lt;span class="s"&gt;``pip install pyodide-build&amp;gt;=0.29.2``&lt;/span&gt;).
&lt;span class="m"&gt;2.&lt;/span&gt; Get the Emscripten version required by your pyodide-build: &lt;span class="s"&gt;``pyodide config get emscripten_version``&lt;/span&gt;.
&lt;span class="m"&gt;3.&lt;/span&gt; Install and activate that Emscripten version using the &lt;span class="s"&gt;`Emscripten SDK (emsdk) &lt;/span&gt;&lt;span class="si"&gt;&amp;lt;https://emscripten.org/docs/getting_started/downloads.html&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;`_&lt;/span&gt;:

&lt;span class="p"&gt;   ..&lt;/span&gt; &lt;span class="ow"&gt;code-block&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="k"&gt;bash&lt;/span&gt;

      git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/emscripten-core/emsdk.git
      &lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;emsdk
      ./emsdk&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;version&amp;gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# use the version from step 2&lt;/span&gt;
      ./emsdk&lt;span class="w"&gt; &lt;/span&gt;activate&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;version&amp;gt;
      &lt;span class="nb"&gt;source&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;emsdk_env.sh

&lt;span class="m"&gt;4.&lt;/span&gt; In any shell where you want to build the wasm wheel, ensure Emscripten is on &lt;span class="s"&gt;``PATH``&lt;/span&gt; (e.g. run &lt;span class="s"&gt;``source /path/to/emsdk/emsdk_env.sh``&lt;/span&gt;).

&lt;span class="gs"&gt;**Build the wheel**&lt;/span&gt;

From the project root, with Emscripten activated and your dev environment active (e.g. &lt;span class="s"&gt;``pixi shell``&lt;/span&gt;):

&lt;span class="p"&gt;..&lt;/span&gt; &lt;span class="ow"&gt;code-block&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt; &lt;span class="k"&gt;bash&lt;/span&gt;

   pyodide&lt;span class="w"&gt; &lt;/span&gt;build

Or with Pixi: &lt;span class="s"&gt;``pixi run wheel-wasm``&lt;/span&gt;.

The wheel will appear in &lt;span class="s"&gt;``dist/``&lt;/span&gt;. PyPI does not yet accept emscripten/wasm32 wheels; host the file elsewhere (e.g. GitHub Releases) and install in Pyodide with &lt;span class="s"&gt;``micropip.install(url)``&lt;/span&gt;. See &lt;span class="s"&gt;`Pyodide: building packages &lt;/span&gt;&lt;span class="si"&gt;&amp;lt;https://pyodide.org/en/stable/development/building-packages-from-source.html&amp;gt;&lt;/span&gt;&lt;span class="s"&gt;`_&lt;/span&gt; for details.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This documentation walks through the Emscripten setup, the build command, and importantly, notes that PyPI doesn't accept WASM wheels yet—you need to distribute them via GitHub Releases or similar and install with &lt;code&gt;micropip.install(url)&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="what-i-actually-pr-d-to-pytensor"&gt;What I actually PR'd to PyTensor&lt;/h2&gt;&lt;p&gt;The changes above represent my weekend exploration, but they weren't what I ultimately contributed back to PyTensor. The Pixi configuration, in particular, was too large of a departure from PyTensor's existing toolchain. PyTensor uses mamba (via &lt;code&gt;environment.yml&lt;/code&gt;) for its development environment, and switching to Pixi would have been a significant change to impose on a project I don't maintain.&lt;/p&gt;
&lt;p&gt;Instead, I re-did the infrastructure changes using &lt;code&gt;pyodide-build&lt;/code&gt; while respecting PyTensor's existing mamba-based workflow. The core change (making Numba optional on WebAssembly) remained, but the development environment configuration was adapted to work with what PyTensor already had in place.&lt;/p&gt;
&lt;p&gt;This is a common lesson in open-source contribution: meeting maintainers where they are matters more than introducing your preferred tooling. The weekend experiment taught me what was needed; the PR reflected what was appropriate. You can see the actual PR here: &lt;a href="https://github.com/pymc-devs/pytensor/pull/1960"&gt;pytensor #1960&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="unfortunately-for-now-nuts-is-gone"&gt;Unfortunately (for now), NUTS is gone&lt;/h2&gt;&lt;p&gt;Unfortunately, &lt;strong&gt;NUTS (No-U-Turn Sampler) doesn't work in WASM&lt;/strong&gt;. 😭&lt;/p&gt;
&lt;p&gt;NUTS is the crown jewel of PyMC. It's the adaptive Hamiltonian Monte Carlo sampler that makes Bayesian inference efficient and robust. The 2022 PyMC Labs demo used NUTS to sample from posteriors in real-time in the browser.&lt;/p&gt;
&lt;p&gt;But here's the thing: this isn't just about Numba being unavailable. The real issue is that none of the modern MCMC sampling backends have WASM support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;JAX&lt;/strong&gt; (used by NumPyro and BlackJAX) has an open GitHub issue &lt;a href="https://github.com/jax-ml/jax/issues/1472"&gt;#1472&lt;/a&gt; from 2019 titled "Jax for Web? (JS api or web assembly guide)" that's still open with no official WASM support&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;nutpie&lt;/strong&gt; (the Rust-based NUTS implementation) doesn't have a WASM build readily available&lt;/li&gt;
&lt;li&gt;The computational demands of Hamiltonian dynamics—computing gradients, simulating trajectories, adapting step sizes—require optimized backends that don't exist in WASM environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This weekend's exploration shows the path to install PyMC in WASM, but you can't use its best sampler. It's like getting a Ferrari delivered to your house, but the dealer forgot to include the keys. You can sit in it, admire the leather seats, and maybe even turn on the radio. But you're not going anywhere fast.&lt;/p&gt;
&lt;p&gt;This represents a fundamental infrastructure gap, not just a missing dependency. Getting NUTS in the browser will require either WASM ports of JAX or nutpie, or entirely new sampling backends designed for browser environments.&lt;/p&gt;
&lt;h2 id="what-does-work"&gt;What does work?&lt;/h2&gt;&lt;p&gt;Despite the NUTS heartbreak, this wasn't a failed experiment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;PyTensor now installs in WASM environments.&lt;/strong&gt; This is non-trivial. PyTensor has C and Cython extensions that need to compile for WebAssembly. Getting that build pipeline working required understanding Pyodide's build system, setting up Emscripten correctly, and making Numba optional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyMC can technically be imported.&lt;/strong&gt; Once PyTensor was installable, PyMC followed. You can define models, create random variables, and work with the API. The foundation is there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Alternative samplers might still work.&lt;/strong&gt; While NUTS is off the table, other samplers—like Metropolis-Hastings or Slice sampling—might be viable for small models. They're slower and less robust than NUTS, but they don't require JIT compilation. I didn't test, but I think this will hold true!&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The roadmap is clearer.&lt;/strong&gt; If someone wants to bring full PyMC to the browser, the path forward is documented. It requires either (a) building WASM support into JAX (a massive undertaking that's been an open request since 2019), (b) creating WASM builds for nutpie, or (c) building entirely new sampling backends designed for browser environments (also non-trivial, but potentially more feasible).&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="a-weekend-well-spent"&gt;A weekend well spent&lt;/h2&gt;&lt;p&gt;Did I achieve my original goal of running PyMC in the browser with NUTS sampling? No. The technical limitations of WASM environments made that impossible with the current architecture.&lt;/p&gt;
&lt;p&gt;But that's the nature of weekend experiments. You explore, you hit walls, you learn. I now understand PyTensor's dependency structure at a deeper level. I've learned how Pyodide builds work and the constraints they impose. I've identified the broader infrastructure gap (MCMC sampling backends lacking WASM support) that needs solving for true browser-based Bayesian inference.&lt;/p&gt;
&lt;p&gt;The dream of running PyMC entirely in the browser isn't dead—it's just waiting for the right infrastructure. Until JAX or nutpie (or something else) supports WASM, we'll keep pushing that car downhill.&lt;/p&gt;
</content></entry><entry><title>Mastering Personal Knowledge Management with Obsidian and AI</title><link href="https://ericmjl.github.io/blog/2026/3/6/mastering-personal-knowledge-management-with-obsidian-and-ai/" rel="alternate"/><updated>2026-03-06T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0a70d5db-2590-3622-84d3-785f1bf45d29</id><content type="html">&lt;p&gt;Folks have asked me how I do personal knowledge management (PKM) at work. The question becomes more pressing when they learn how many projects and people I need to interact with on a weekly basis. At the time of writing, I manage twelve people across two teams, each handling 2-4 projects of their own. That's a lot of context to keep straight.&lt;/p&gt;
&lt;p&gt;I decided to document what I'm doing for PKM. Hopefully it serves as inspiration for you.&lt;/p&gt;
&lt;p&gt;I've written before about &lt;a href="../../../../2020/12/15/building-a-personal-knowledge-graph-on-obsidian/"&gt;why I chose Obsidian&lt;/a&gt;; this post shows how that decision evolved with AI integration over five years.&lt;/p&gt;
&lt;h2 id="the-plain-text-decision"&gt;The plain text decision&lt;/h2&gt;&lt;p&gt;In 2022, I decided to make personal knowledge management a priority at work. I faced a choice: Confluence, OneNote, or a new kid on the block, Obsidian. I chose plain text and graphs. I chose Obsidian. I chose not to lock my data inside a vendor system. I chose freedom and sovereignty for my information.&lt;/p&gt;
&lt;p&gt;That decision was prescient in ways I couldn't have predicted. Most of us normies back then wouldn't have guessed that plain text would be exactly the right format for 2025 and 2026 era knowledge management. The visionaries saw it coming; I just got lucky because I loved the graph view in Obsidian and thought of it as a really cool tool. But holy smokes, has that choice paid off.&lt;/p&gt;
&lt;p&gt;Text files are as primitive as it gets: no proprietary formats, no vendor lock-in, just files that can be read on any system. When AI coding agents arrived, my vault was already in a format they could process natively. No migration needed. No conversion layer. No API integration. The simplicity I chose became an unlock I never planned for.&lt;/p&gt;
&lt;h2 id="the-core-system"&gt;The core system&lt;/h2&gt;&lt;p&gt;My Obsidian vault is built around distinct note types. Monthly collections of daily bullet journals capture my day-to-day activities, one note per month with a running log of meetings and work. Meeting notes follow a structured template. People notes are dossiers for everyone I work with (to put it in CIA terms, I keep a file on everyone I interact with regularly). Project notes act as control towers, linking out to meetings, people, and status updates. A miscellaneous collection handles everything else. The structure was inspired in part by Thiago Forte's numbered folder system, though I've simplified it over time.&lt;/p&gt;
&lt;p&gt;The most important thing isn't my specific implementation. It's that I have a system at all, and it's documented in an &lt;code&gt;AGENTS.md&lt;/code&gt; file so my coding agents understand it too.&lt;/p&gt;
&lt;h2 id="ingesting-information"&gt;Ingesting information&lt;/h2&gt;&lt;p&gt;The lifecycle of my workflow starts with ingestion. Meeting notes arrive as transcripts or AI-generated summaries. In the past, structuring these was tedious work. Now I paste them into OpenCode and my meeting notes skill handles the rest.&lt;/p&gt;
&lt;p&gt;The skill knows the template I want. It handles various input formats: AI-generated summaries, transcripts with good speaker assignments, and transcripts with poor speaker assignments. I flag the quality when I know it's bad. The skill extracts key information and formats everything consistently. For one-on-ones, it ensures notes are attached to both the meeting log and the person's individual page, so I can track the full history of our conversations.&lt;/p&gt;
&lt;p&gt;Beyond meetings, I ingest PowerPoints, Word docs, PDFs, and Excel spreadsheets into my vault as contextual information. The key insight is getting everything into plain text format. For Word documents, a Python script converts them to plain text using &lt;code&gt;python-docx&lt;/code&gt;, which is then printed to the terminal or dumped to disk at &lt;code&gt;/tmp&lt;/code&gt;, both of which are readable by a coding agent. Even lightly misformatted plain text contains enough information density for a coding agent to read and summarize.&lt;/p&gt;
&lt;p&gt;For PowerPoints, I use dual parsing paths. One path extracts the XML structure directly using &lt;code&gt;python-pptx&lt;/code&gt;. The second path converts each slide to an image using &lt;code&gt;libreoffice&lt;/code&gt; and &lt;code&gt;PIL&lt;/code&gt;, captions it with a vision-language model via APIs, and strings the captions together into a coherent narrative. Combined, I estimate that I can get a 90-95% accurate textual representation. PDFs follow a similar pattern: text extraction for normal PDFs, image captioning for scanned documents.&lt;/p&gt;
&lt;p&gt;Excel spreadsheets are read directly by the coding agent using &lt;code&gt;openpyxl&lt;/code&gt;, not &lt;code&gt;pandas&lt;/code&gt;. The key difference matters: &lt;code&gt;pandas&lt;/code&gt; assumes an established table structure, but real-world spreadsheets are messy. With &lt;code&gt;openpyxl&lt;/code&gt;, the agent can read the granular cellular structure across each sheet, identifying merged cells, free text scattered in random locations, and arbitrary layouts. This structural mapping follows a progressive reveal principle: the agent first identifies the spreadsheet's architecture without necessarily reading every cell's contents, then zooms into relevant sections. This approach handles the chaos of actual spreadsheets far better than forcing everything into a tabular assumption. It's powerful when I need to understand financial data without being a finance person.&lt;/p&gt;
&lt;h2 id="managing-and-maintaining"&gt;Managing and maintaining&lt;/h2&gt;&lt;p&gt;With information in the vault, the next phase is keeping it current. With twelve people across two teams, there are a lot of details I don't pick up or retain in my working memory. That's why external memory matters. Without it, things would fall through the cracks.&lt;/p&gt;
&lt;p&gt;When I hit a context block (when I look up a project or person and realize something's missing), I trigger a "sweep". My instructions to the coding agent are to update my people notes and/or project notes based on source material present in the vault. People and project notes are always derivative from sources, so any updates must include quotations from those source notes. I stay in the loop for verification. Hallucinations are rare, maybe once every four or five sweeps, and usually trace back to inaccurate transcripts rather than agent errors.&lt;/p&gt;
&lt;p&gt;This is incredibly helpful for how I interact with people. My assumption is that I'm going to be forgetful. My external memory will be approximately correct, and I have a process for keeping it refined over time. So I can rely more on the vault instead of second-guessing myself based on incomplete memory. It tempers how I think about interacting with someone, not by changing my mind about them, but by giving me confidence that I'm not missing something important.&lt;/p&gt;
&lt;p&gt;There are ethical boundaries. I don't capture personal details if people aren't comfortable with that. The dossiers are professional, not invasive.&lt;/p&gt;
&lt;p&gt;Periodically, I do retrieval practice. This is how we make information stick; read "Make It Stick" to learn more. Review looks like this: I take my people notes and project notes and ask what's missing. Is there a piece of knowledge I remember that isn't captured? If yes, I fill in the blanks. I also check whether claims are substantiated with links and quotes. This fact-checking pass keeps the vault trustworthy and protects me from remembering something erroneous. A spell-checker list handles transcription errors, and my &lt;code&gt;AGENTS.md&lt;/code&gt; links to &lt;code&gt;HEARTBEAT.md&lt;/code&gt; to sanitize the vault of inaccurate information.&lt;/p&gt;
&lt;h2 id="producing-and-sharing"&gt;Producing and sharing&lt;/h2&gt;&lt;p&gt;The final phase is producing outputs for others. I curate what gets published rather than exporting everything. The agent creates a publishable version based on my guidance. I haven't settled on hard rules for curation yet. I'd rather review and decide at publish time than tag things as publishable during capture. That workflow feels right to me.&lt;/p&gt;
&lt;p&gt;For Confluence, a Python script publishes markdown directly, with YAML front matter defining the space and parent page. For GitHub users, notes can become Gists via the GitHub CLI. With the appropriate skills, Markdown files transform into HTML presentations, and with web technologies, those presentations become interactive. For Jira, a colleague created a skill that writes Jira tickets. We firmly believe that humans shouldn't be filling forms out; AI should be filling forms for us.&lt;/p&gt;
&lt;p&gt;PowerPoint decks can be generated via Python scripts. Word documents come from markdown via Pandoc. The scripts run with uv, and LibreOffice handles conversions.&lt;/p&gt;
&lt;p&gt;Each script maintains its own environment using PEP 723 inline script metadata. This means dependencies are declared at the top of each script in a special comment block:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# /// script&lt;/span&gt;
&lt;span class="c1"&gt;# dependencies = [&amp;quot;python-docx&amp;quot;, &amp;quot;python-pptx&amp;quot;, &amp;quot;pandas&amp;quot;]&lt;/span&gt;
&lt;span class="c1"&gt;# ///&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When I run &lt;code&gt;uv run script.py&lt;/code&gt;, uv automatically creates an isolated environment with just those dependencies, executes the script, and cleans up. No virtual environments to manage. No &lt;code&gt;requirements.txt&lt;/code&gt; files scattered everywhere. No "works on my machine" problems.&lt;/p&gt;
&lt;h2 id="the-role-of-agent-skills"&gt;The role of agent skills&lt;/h2&gt;&lt;p&gt;Agent skills effectively encode procedural knowledge into executable markdown. Over time, it compounds; on fewer and fewer occasions do I need to repeat instructions, which is incredibly liberating. The model infers which skill to use most of the time. When it doesn't, I correct it explicitly and ask the coding agent to update the skill file for the future as well.&lt;/p&gt;
&lt;p&gt;Designing a skill means thinking about the desired output and the tools needed to get there. I discover edge cases in the wild and update immediately. The earlier errors are caught, the better.&lt;/p&gt;
&lt;h2 id="what-s-still-friction"&gt;What's still friction&lt;/h2&gt;&lt;p&gt;One pain point remains. I want to ingest Office files by pasting a URL, but I still need to download a copy first, then feed that copy to the agent skill. Programmatic access to cloud documents would eliminate this step. From the user side, nothing else would change. I'd just paste the URL and go.&lt;/p&gt;
&lt;p&gt;But even with this friction, the system pays for itself. Knowledge management overhead dropped from thirty to forty percent of my time down to less than ten percent. I fix errors as I encounter them rather than scheduling dedicated maintenance. That recovered bandwidth goes toward better thinking and context gathering.&lt;/p&gt;
&lt;h2 id="getting-started"&gt;Getting started&lt;/h2&gt;&lt;p&gt;What stops people from building systems like this? I believe it is two things: imagination and technical skill. You need imagination to envision converting diverse file formats into plain text. You need technical skill to know that it's possible.&lt;/p&gt;
&lt;p&gt;The two feed each other. I experienced this with web technologies. Before I got familiar with building stuff on the web, I wondered what was even possible. Once I actually built things, I knew. Technical skill feeds your imagination, and imagination drives you to learn more technical skills.&lt;/p&gt;
&lt;p&gt;For those starting without technical skills, use AI to learn programming. Find a language with a supportive human community to verify what you learn. AI hallucinates, and you need other people around you to help apply judgment and skill to AI outputs. You also need critical thinking skills and the initiative to act on what agents produce.&lt;/p&gt;
&lt;h2 id="skills-you-can-use-today"&gt;Skills you can use today&lt;/h2&gt;&lt;p&gt;If you want to experiment with agent skills, here are some I've published:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/tree/main/skills/html-presentations"&gt;html-presentations&lt;/a&gt; - Turn markdown into gorgeous HTML slides&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/tree/main/skills/gh-daily-timeline"&gt;gh-daily-timeline&lt;/a&gt; - See your GitHub activity for any given day&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/tree/main/skills/gh-activity-summary"&gt;gh-activity-summary&lt;/a&gt; - Generate a plain-language summary of your GitHub work over any time period&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/tree/main/skills/publish-to-google-docs"&gt;publish-to-google-docs&lt;/a&gt; - Push markdown notes to Google Docs&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-bigger-picture"&gt;The bigger picture&lt;/h2&gt;&lt;p&gt;With such a system in place, repetitive, monotonous, and manual work can be offloaded to computers and AI. With a personal knowledge system, we can carry a broader scope of responsibilities and grow into new challenges for two reasons: we can externalize our memory more easily, and we can format information in ways that fit our brains.&lt;/p&gt;
&lt;p&gt;I'm not asking people to do more at the same time. I'm asking them to expand their dynamic range over time so they're not stuck doing the same old boring thing over and over. That repetitive monotonous stuff should have been given away to AI and computers a long time ago.&lt;/p&gt;
&lt;p&gt;This is useful for your career. It keeps things interesting. Every day that you make an incremental but permanent improvement compounds over time.&lt;/p&gt;
&lt;p&gt;The vignettes I've shared are not a prescription. Rather, I hope you treat them as an invitation. Plain text plus coding agents is a powerful combination. Your system will look different from mine, and that's part of the point. Experiment and explore, and find what works for you.&lt;/p&gt;
</content></entry><entry><title>How to stay in control when doing EDA with coding agents</title><link href="https://ericmjl.github.io/blog/2026/2/13/agentic-eda/" rel="alternate"/><updated>2026-02-13T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:f7d7fdf8-57b8-3130-99d4-c72d42aea0cc</id><content type="html">&lt;p&gt;Speed without control is just chaos.&lt;/p&gt;
&lt;p&gt;I've seen teammates compress a week and a half of analysis work into half a day using coding agents. That's a 5-10x speedup. But here's the thing: that speed only matters if you stay in the driver's seat. Otherwise you're not doing data science, you're just generating artifacts.&lt;/p&gt;
&lt;p&gt;The real unlock isn't that agents write code fast. It's that they can be guided through a structure that keeps you in control of the analysis.&lt;/p&gt;
&lt;h2 id="the-problem-isn-t-speed-it-s-agency"&gt;The problem isn't speed, it's agency&lt;/h2&gt;&lt;p&gt;Coding agents are eager. Give them a CSV file and they'll open it, generate a dozen plots, and dump a wall of code before you've finished describing what you're actually looking for. That feels productive. It isn't.&lt;/p&gt;
&lt;p&gt;The problem is that you've lost the thread. You didn't formulate a clear question. You didn't think through what the x-axis and y-axis should be. You're now reacting to whatever the agent produced, rather than steering toward an answer.&lt;/p&gt;
&lt;p&gt;I've developed a different approach, codified in two skills I use with my coding agents: &lt;a href="https://github.com/ericmjl/skills/tree/main/skills/scientific-eda"&gt;scientific-eda&lt;/a&gt; for exploratory data analysis and &lt;a href="https://github.com/ericmjl/skills/tree/main/skills/ml-experimentation"&gt;ml-experimentation&lt;/a&gt; for machine learning experiments. The pattern is the same in both: slow down first, gate on artifacts (plots, tables, etc.), and structure the session so both you and the agent can follow what happened.&lt;/p&gt;
&lt;h2 id="slow-down-first-the-socratic-opening"&gt;Slow down first: the Socratic opening&lt;/h2&gt;&lt;p&gt;The first design principle is counterintuitive: slow down before you speed up.&lt;/p&gt;
&lt;p&gt;When you invoke the scientific-eda skill, the agent does not immediately load your data and start plotting. Instead, it asks you questions. What's the problem context? What are you hoping to learn or decide? What constraints matter?&lt;/p&gt;
&lt;p&gt;From the skill definition:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Do not open the data file and start coding or plotting. Ask for or confirm: the problem context—biological, chemical, or data-science question; what the user hopes to learn or decide; and any constraints.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's also an explicit guardrail: "ask 'why' before executing." When you request a specific plot or table, the agent briefly asks what question or decision it serves. This isn't bureaucracy. It's alignment. The agent is checking that you've thought through the request before it spends your time executing it.&lt;/p&gt;
&lt;p&gt;This Socratic opening feels slower. But it prevents the far more common waste: generating plots you didn't need, going down rabbit holes you can't explain, and ending up with a folder full of artifacts and no clear answer.&lt;/p&gt;
&lt;h2 id="gate-everything-on-artifacts"&gt;Gate everything on artifacts&lt;/h2&gt;&lt;p&gt;The second design principle is more specific: one artifact at a time.&lt;/p&gt;
&lt;p&gt;If you can't describe what you want, you're not ready to execute. The agent waits. You think. You describe. Then the agent generates exactly what you asked for.&lt;/p&gt;
&lt;p&gt;For a plot, this means articulating the x-axis, the y-axis, and what pattern you're looking for. What would confirm or refute your hypothesis? If you can't answer, the analysis isn't ready to run.&lt;/p&gt;
&lt;p&gt;For a table, this means specifying the columns, rows, and aggregation level. What comparison are you trying to make? What decision will this table inform? A vague request like "show me the data" isn't actionable. "Show me the mean expression level by treatment group" is.&lt;/p&gt;
&lt;p&gt;This is a forcing function for clarity. Describing an artifact precisely forces you to articulate the question you're actually asking. The tradeoff is worth it. You give up a bit of speed up front for precision in execution. And because the agent can generate code in seconds rather than minutes, the net result is still a massive speedup. My teammates went from 1.5-2 weeks to half a day. The precision tax is negligible compared to the execution dividend.&lt;/p&gt;
&lt;h2 id="the-session-structure"&gt;The session structure&lt;/h2&gt;&lt;p&gt;Here's where structure becomes a feature, not overhead.&lt;/p&gt;
&lt;p&gt;Each analysis session is a timestamped folder. The naming convention is ISO datetime plus a descriptive slug:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;analysis/
  2025-02-05T14-30-00-protein-binding/
    journal.md      # append-only; shape, actions, findings
    plots/          # WebP figures only
    scripts/        # disposable PEP723 scripts; uv run
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's walk through what each piece does.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;journal.md&lt;/strong&gt; is the memory. Before each action, the agent reads the journal. After each action, it appends what happened. Entries get timestamped and tagged: &lt;code&gt;[SHAPE]&lt;/code&gt; for data structure discoveries, &lt;code&gt;[PLOT]&lt;/code&gt; for visualizations, &lt;code&gt;[FINDING]&lt;/code&gt; for observations, &lt;code&gt;[NEXT]&lt;/code&gt; for suggested next steps. The journal is scannable. It's also the entry point for anyone (including future you) who wants to understand what happened without reading the code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;plots/&lt;/strong&gt; holds all figures from the session. The skill specifies WebP format for smaller file sizes, though that's a minor detail.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;scripts/&lt;/strong&gt; contains disposable Python scripts. Each script has PEP723 inline metadata at the top, declaring its own dependencies. You run them with &lt;code&gt;uv run script.py&lt;/code&gt; from the session folder. No environment wrangling. No "which virtualenv am I in?" confusion. One script, one plot.&lt;/p&gt;
&lt;p&gt;The structure serves two purposes. First, it gives the agent a clear protocol to follow: read journal, execute, append to journal, suggest next step. Second, it leaves a trace that any human can follow. You can read the plan, then the journal, then the report, and understand the entire analysis without touching the code.&lt;/p&gt;
&lt;h2 id="what-changes-in-team-conversations"&gt;What changes in team conversations&lt;/h2&gt;&lt;p&gt;Something unexpected happened when I started using this approach with teammates.&lt;/p&gt;
&lt;p&gt;The conversations changed. We stopped asking "why did you write it that way?" and started asking "why did the agent write it that way?" The ego attached to code ownership evaporated. We could critique the work without critiquing each other.&lt;/p&gt;
&lt;p&gt;This matters more than I expected. In the pre-agent world, I'd invest 50-70% of my mental energy on implementation details: wrangling data frames, handling edge cases, debugging syntax errors. That labor created attachment. When someone questioned my code, it felt like they were questioning my thinking.&lt;/p&gt;
&lt;p&gt;Now the agent writes the code. I focus on the questions and verify the work. My teammates and I have more productive scientific conversations because we're discussing the analysis, not defending the implementation. We check the agent's work together, and if something's wrong, we just ask the agent to fix it.&lt;/p&gt;
&lt;h2 id="design-for-the-human"&gt;Design for the human&lt;/h2&gt;&lt;p&gt;The pattern that emerged is simple: if you design for the human, the agent follows.&lt;/p&gt;
&lt;p&gt;The structure that makes your analysis traceable is the same structure that keeps the agent aligned. The journal that helps future-you understand what happened also helps the agent decide what to do next. The artifact-gating that forces you to think clearly also gives the agent precise instructions to execute.&lt;/p&gt;
&lt;p&gt;You stay in control by slowing down at the decision points, describing what you want before you get it, and keeping a running record of what happened. The agent becomes a force multiplier rather than a loose cannon.&lt;/p&gt;
&lt;p&gt;The skills I've linked here are just text files. They're prompts, structured in a way that an LLM-based coding agent can follow. You can copy them, modify them, or write your own. The key insight isn't in any particular skill, it's in the design pattern: gate analysis on artifacts, structure sessions with journals, and make the human's job explicit before the agent's job begins.&lt;/p&gt;
&lt;p&gt;The 5-10x speedup is real. But the real win is that you get to stay the scientist.&lt;/p&gt;
</content></entry><entry><title>How to Do Agentic Data Science</title><link href="https://ericmjl.github.io/blog/2026/2/1/how-to-do-agentic-data-science/" rel="alternate"/><updated>2026-02-01T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:60e44946-3c79-3457-bbcf-b10ead198a9e</id><content type="html">&lt;p&gt;Having tasted what agentic coding could look like for software development, I wanted to know what it would look like for data science - this meant training machine learning models and answering scientific questions. So I started experimenting, at work, and on my own at home as well. Here are ten lessons I've learned from my experiments thus far.&lt;/p&gt;
&lt;h2 id="1-be-prescriptive-in-your-prompting"&gt;1. Be prescriptive in your prompting&lt;/h2&gt;&lt;p&gt;Similar to building software, you need to know exactly what you want and how you'll evaluate the outcome. The difference, however, is as follows: With software, you will often know what you need to build, but with data science, you can only know what hypotheses need to be verified, which means you will need to iterate your way to the answer. Nonetheless, it is possible to leverage coding agents to move quickly.&lt;/p&gt;
&lt;p&gt;The parallels are striking: if you frame each question you ask in terms of an observable outcome, you can set up your coding agent to write code that produces an output that can be evaluated for correctness, just like with software tests!&lt;/p&gt;
&lt;p&gt;Here, your ability to describe precisely the hypothesis you're exploring, and the ability to describe in precise language what the answer would look like if the hypothesis held true or not, are critical components of what enables the coding agent to figure out what needs to be counterfactually true (within the codebase or the data) in order for your hypothesis to hold true.&lt;/p&gt;
&lt;p&gt;Here is an example from my work. In a machine learning experiment with synthetic data, I wanted to hit 100% sequence editing performance. (It was synthetic data after all!) The coding agent hit a scenario where it was only doing 25%. With the clear goal in mind, it proposed edits to the code, edited the code, and re-ran experiments until it hit 100%. All without cheating; I know, because I checked!&lt;/p&gt;
&lt;h2 id="2-strong-patterns-in-the-file-system"&gt;2. Strong patterns in the file system&lt;/h2&gt;&lt;p&gt;The agent, like humans, needs a predictable place for experiments. Similar to how a software repo has a conventional layout (src/, tests/, and so on), your experiments need a conventional layout so the agent knows where to put things and where to look. Within the &lt;a href="https://github.com/ericmjl/skills/tree/main/skills/ml-experimentation"&gt;experimentation skill that I wrote&lt;/a&gt;, I instruct the coding agent to do its work inside an &lt;code&gt;experiments&lt;/code&gt; folder. Underneath that, for each experiment, we have datetime-prefixed subfolders, in which, there's a README file, a &lt;code&gt;plots&lt;/code&gt; directory, a &lt;code&gt;data&lt;/code&gt; directory, a &lt;code&gt;scripts&lt;/code&gt; directory. Naming things logically helps, but the scheme matters more than the exact names. Coding agents will follow the patterns you already have.&lt;/p&gt;
&lt;h2 id="3-put-logging-instructions-in-agents.md"&gt;3. Put logging instructions in AGENTS.md&lt;/h2&gt;&lt;p&gt;With software, the feature one ask's a coding agent to build either succeeds or fails, and this can be automatically verified using programmatically-runnable unit and integration tests. With data science, experimental runs produce logs and metrics, but aren't easily boolean pass/fail like software tests. In both cases, however, and the agent can introspect logs to figure out what to change!&lt;/p&gt;
&lt;p&gt;Your AGENTS.md file should include instructions for putting enough logging in place so the LLM can introspect what's going on during the experiment. I've written elsewhere about &lt;a href="../../../../2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/"&gt;how to teach your coding agent with AGENTS.md&lt;/a&gt; and &lt;a href="../../../1/17/how-to-build-self-improving-coding-agents-part-1/"&gt;using AGENTS.md as repository memory&lt;/a&gt; for self-improving agents. Pair that with tools that run code in the terminal so the agent gets logs it can read. When the agent can read the logs, it can figure out what's wrong and what to change.&lt;/p&gt;
&lt;p&gt;In my work, logging and printing to terminal were what let my agent fix a masking strategy that was only yielding 25% correctness. It read the logs, proposed a fix, re-ran, and got to where we needed to be. No intervention on my part. A 3 day experiment became 20 minutes.&lt;/p&gt;
&lt;h2 id="4-give-it-report-writing-skills"&gt;4. Give it report-writing skills&lt;/h2&gt;&lt;p&gt;The agent can write code and read mountains of logs, but you need something else: a human-readable summary of what it observed and what looked weird, so you can triage without re-reading every log. Give your coding agent instructions (e.g. in an &lt;a href="https://agentskills.io/home"&gt;agent skill&lt;/a&gt;) to write out in plain language what it observed during the model evaluation phase. It should read execution logs throughout the run. Tell it to write down anything that looks weird for follow-up. If something is off, it should say so. You get a readable summary and a list of things to dig into.&lt;/p&gt;
&lt;p&gt;For reports (e.g. &lt;code&gt;reports.md&lt;/code&gt;), encode in the skill that every table and every plot must be scrutinized. Ensure that plots are generated for every table, and that someone (you or the agent, with you verifying) carefully checks for inconsistencies between AI-generated plots and the tables they are supposed to reflect. The agent can miss things. It is valid to ask the AI to check its own work, but only if you have an idea of exactly where it is wrong and you tell it as such. Vague "double-check this" rarely helps; "the values in figure 2 do not match the second column of table 1" gives the agent something it can fix.&lt;/p&gt;
&lt;h2 id="5-have-the-agent-keep-an-append-only-journal-of-observations"&gt;5. Have the agent keep an append-only journal of observations&lt;/h2&gt;&lt;p&gt;Within the skill, instruct the agent to keep a single file (e.g. &lt;code&gt;notes.md&lt;/code&gt; or &lt;code&gt;journal.md&lt;/code&gt;) that it is told to only append to, never overwrite. The journal is not just for the agent. You should add to it too: things you noticed while looking at the data, gut feelings, weird patterns. It becomes a running log of what was going on, from both sides, that you can go back and summarize later. The point is to capture the thought process while you are doing the work.&lt;/p&gt;
&lt;h2 id="6-have-the-agent-generate-diagnostic-plots-for-you"&gt;6. Have the agent generate diagnostic plots for you&lt;/h2&gt;&lt;p&gt;Logs and plots are complementary: logs are agent-accessible, but plots are human-accessible versions of the same underlying performance data. Have the agent generate diagnostic plots for you. The agent can propose fixes from the logs, but it can't build your intuition; you're the one who has to smell when something is off. Nothing beats looking at the data yourself, otherwise you never build intuition for what's happening! I still looked at the logs and plots myself to make sure the metrics were real and the agent wasn't hallucinating. Your prior experience is what lets you smell when something is off.&lt;/p&gt;
&lt;h2 id="7-instruct-the-agent-to-write-the-minimalist-version-first"&gt;7. Instruct the agent to write the minimalist version first&lt;/h2&gt;&lt;p&gt;With software, you run tests in seconds. With ML, you're tempted to train for hours and rush to the real data and the real training run. As a human, you don't want to "waste" time proving out the pipeline when you could just run the full thing, but that mentality is exactly what makes you unable to debug machine learning code on a tight loop. That temptation is exactly why you should instruct the agent to do the opposite: write the minimalist version first, then use it to work out elementary errors before scaling up.&lt;/p&gt;
&lt;p&gt;That means train for one iteration, not even one epoch. Use miniature versions of the final model (e.g. a tiny custom deep net with the same architecture but a fraction of the parameters). Check for shape errors, data-loading bugs, and that the forward pass runs end to end. All the sanity checks you would do manually to prove that things work, but that you are tempted to skip. Encode in AGENTS.md that the agent must implement and run this minimal version before moving to full-scale training. The agent does not have your impatience; use that to your advantage. Once the minimal run passes, you can scale up with confidence.&lt;/p&gt;
&lt;h2 id="8-ask-the-coding-agent-to-guide-you-through-step-by-step"&gt;8. Ask the coding agent to guide you through step by step&lt;/h2&gt;&lt;p&gt;I do this often with large software refactors within &lt;code&gt;canvas-chat&lt;/code&gt;, in which I ask the coding agent to prioritize for me a list of manual checks I need to look at. This is particularly helpful when I'm (a) context switching back into the project, or (b) running on fumes but my gut tells me we're so close to the end. (Though really, you shouldn't be doing any work if you're close to dozing off at 10:30 pm...)&lt;/p&gt;
&lt;p&gt;The same applies to data science and data exploration! After having the coding agent autonomously execute on your experiment, you can have it walk through what it's done step by step, giving you the space to operate at your pace -- at the speed of your thought! Of course, if you're in a better state than merely "running on fumes", you can (and should) treat the coding agent as a research partner and ask questions back to critically evaluate whether the output is correct or not. What I have found is that there will still be unexplored paths that need to be trodden, and you can send a coding agent off on that direction on the side.&lt;/p&gt;
&lt;h2 id="9-learn-the-vocabulary-the-coding-agent-uses"&gt;9. Learn the vocabulary the coding agent uses&lt;/h2&gt;&lt;p&gt;Pay attention to the terms the agent uses when it describes what it did. You can reuse that vocabulary in future prompts and get more precise results. For example, in developing canvas-chat, I used this non-optimal verbiage in my prompt:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;ok, I see, the default node size made it such that the next/prev buttons were hidden away. Can we make the pagination controls visible regardless of the size of the node?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Cursor's agent replied with something like "making the pagination toolbar sticky". That gave me a more compact way to express exactly what I need the next time. If you don't know the vocab at first, this is a great way to expand your technical vocabulary too.&lt;/p&gt;
&lt;h2 id="10-for-exploration-treat-the-agent-as-an-executor-that-follows-your-curiosity"&gt;10. For exploration, treat the agent as an executor that follows your curiosity&lt;/h2&gt;&lt;p&gt;What you don't want in agentic data exploration is for the coding agent to hand you a boatload of output and leave you no room to follow your own curiosity. Flip the table: treat the agent as an executor of your ideas. You lead; it follows. Instruct it that it is not allowed to race ahead. It should only execute on the one thing you want, and it should ask you questions to clarify and narrow down what you actually want before it goes and does it. In other words, it is there to be a jazz partner for your data exploration.&lt;/p&gt;
&lt;p&gt;You can run that partnership a few ways. One is to have the agent write scripts that produce plots on disk; you run them, look at the output, then ask for the next thing. Another is to go one level higher and work inside &lt;a href="https://marimo.io"&gt;Marimo&lt;/a&gt; notebooks, using Marimo's reactive execution so you go one cell at a time, one question at a time. I've written about &lt;a href="../../../../2025/10/28/use-coding-agents-to-write-marimo-notebooks/"&gt;using coding agents to write Marimo notebooks&lt;/a&gt; if you want to try that path.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;The agents handle the implementation. You handle the inquiry. The ten practices above - prescriptive goals, clear structure, logging, reports, an append-only journal, diagnostic plots and your own eyes on the data, the minimalist version first, having the agent guide you step by step when you need it, learning the agent's vocabulary, and in exploration keeping the agent as your jazz partner - are what make that partnership work. I've spent nearly a decade training ML models by hand, so I know what I want, and I have developed a sense of taste for what success looks like. You can get to the same level of taste with AI assistance, but you must work for it. I'll write separately about how I'm learning new things with AI. The point is not to hand off the science, but to do more of it!&lt;/p&gt;
</content></entry><entry><title>Model feel, fast tests, and AI coding that stays in flow</title><link href="https://ericmjl.github.io/blog/2026/1/25/model-feel-fast-tests-and-ai-coding-that-stays-in-flow/" rel="alternate"/><updated>2026-01-25T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:e17e872c-bf5f-3a8b-8ad9-789f8afa713d</id><content type="html">&lt;p&gt;Most of the conversation about AI coding models focuses on performance metrics. Benchmarks, evals, pass rates, latency. Useful stuff, but it misses the part that actually shapes my day-to-day: what it &lt;em&gt;feels&lt;/em&gt; like to work with the model.&lt;/p&gt;
&lt;p&gt;Once you start using LLMs as coding agents, the qualitative experience becomes a throughput issue. It affects how often you intervene, how much you trust what is happening, and whether you stay in flow or spend your time cleaning up weird breakage.&lt;/p&gt;
&lt;p&gt;Two axes keep showing up for me.&lt;/p&gt;
&lt;p&gt;First is time horizon and supervision style: long-horizon autonomy versus short-horizon iteration.&lt;/p&gt;
&lt;p&gt;Second is personality and verbosity: how the model behaves when it is wrong, how much it narrates, and whether it stays constructive or spirals into apology loops.&lt;/p&gt;
&lt;p&gt;There is also a third ingredient that ends up mattering as much as the model: the agentic harness. By that I mean the tools and checks that the agent can run to verify it did not break behavior, &lt;em&gt;and&lt;/em&gt; whether the harness gives you streaming and visual feedback—a live trace of what the model is doing—or leaves you staring at a spinner until the answer drops. Good harness beats model swapping more often than I expected.&lt;/p&gt;
&lt;h2 id="long-horizon-autonomy-vs-short-horizon-iteration"&gt;Long-horizon autonomy vs short-horizon iteration&lt;/h2&gt;&lt;p&gt;I call it "Opus-feel" when a model has that "ask and it shall be given" vibe with a longer time horizon. You describe what you want, it runs for a while, and it comes back with a plausible scaffold. It is great for momentum.&lt;/p&gt;
&lt;p&gt;I call it "Sonnet-feel" when a model leans toward shorter-horizon iteration. It works better when you are walking through a real codebase step by step, keeping changes small enough that you can validate what happened, correct course, and keep going.&lt;/p&gt;
&lt;p&gt;Another way to put it is that long-horizon autonomy pushes you toward a spec-and-review loop, while short-horizon iteration pushes you toward a steer-and-verify loop. Both can be productive. They just fail differently.&lt;/p&gt;
&lt;p&gt;In a sufficiently large codebase, you cannot rely solely on long-horizon autonomy where you ask for something with a vague description and hope it lands cleanly. You are not always guaranteed something well organized, especially when the job is refactoring rather than greenfield scaffolding.&lt;/p&gt;
&lt;p&gt;A concrete example for me came from Canvas chat. At the time, everything was tied to &lt;code&gt;app.js&lt;/code&gt; and &lt;code&gt;app.py&lt;/code&gt;. When I wanted to refactor things into plugins, I needed to dogfood a plugin pattern in the codebase itself.&lt;/p&gt;
&lt;p&gt;Long-horizon autonomy struggled here. It could generate a plugin pattern, but it was not great at the careful, incremental work of extracting behavior out of a monolith and into a clean plugin boundary.&lt;/p&gt;
&lt;p&gt;Walking bit by bit with Sonnet or Sonnet-quality models was a very different experience. The big win was that I could study the LLM traces live (the tool calls and file edits it proposes step by step) and see where edits were being made. If I noticed a feature handler getting added to &lt;code&gt;app.js&lt;/code&gt; when it clearly belonged in a plugin file, I could intervene immediately and ask, "Why is that thing over there in &lt;code&gt;app.js&lt;/code&gt;? Why is it not inside the plugin file instead?" That kind of interactive, traceable work is where the short-horizon models shine.&lt;/p&gt;
&lt;p&gt;Examples from my own testing, with all the usual caveats: Opus-4.5 (Anthropic), GPT-5.2 (OpenAI), and GLM-4.7 (z.ai) have been solid for the long-horizon, get-it-moving-fast mode. Minimax M 2.1 (OpenCode Zen) feels closer to the short-horizon mode for me. Composer-1 (Cursor) also feels closer to that style. I suspect GPT-4o and GPT-5.1 (both OpenAI) might land there too, but I have not really test-driven them.&lt;/p&gt;
&lt;p&gt;The practical takeaway is that I now switch modes on purpose. When I need speed and initial momentum, I reach for long-horizon autonomy. When I need control, I choose a short-horizon model so I can babysit the work, watch the traces, and intercept it when it tries to do something clever in the wrong place.&lt;/p&gt;
&lt;h3 id="the-harness-lesson-cypress-beat-model-hopping"&gt;The harness lesson (Cypress beat model hopping)&lt;/h3&gt;&lt;p&gt;One more lesson from that period: I did a bunch of model hopping, trying to find something that would fix a particular class of behavioral breakage.&lt;/p&gt;
&lt;p&gt;The most frustrating failures were not subtle logic bugs. They were basic syntax errors introduced during tool-call patching, unclosed brackets, unclosed parentheses, that kind of thing. When that happens, you do not get a slightly-wrong feature, you get a page that fails to load. Debugging it manually is fine the first time, and infuriating on the seventh.&lt;/p&gt;
&lt;p&gt;The thing that actually moved the needle was listening to my colleague Anand Murthy and instantiating Cypress tests. A simple automated page reload catches those failures immediately. It shifts the pain earlier, gives the agent a verification loop it can run on demand, and turns "agentic coding" into something I can trust.&lt;/p&gt;
&lt;p&gt;Here is the dumbest possible example, taken straight from the Cypress suite in canvas-chat. It is not fancy, and that is the point. It catches "the page does not load" failures quickly.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nx"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Help Modal and Auto-Layout&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clearLocalStorage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clearIndexedDB&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;visit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;opens and closes help modal&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#help-btn&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#help-modal&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;be.visible&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#help-close&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;#help-modal&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;should&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;not.be.visible&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Beyond model choice, a great agentic harness matters. If your harness includes tests that a coding agent can run to verify no behavioral breakage, you get to move faster with more confidence, regardless of which model you are using.&lt;/p&gt;
&lt;h2 id="verbosity-attitude-and-the-cost-of-being-wrong"&gt;Verbosity, attitude, and the cost of being wrong&lt;/h2&gt;&lt;p&gt;The other axis that became obvious once I started pressure testing models is verbosity and its associated feel.&lt;/p&gt;
&lt;p&gt;I tried Gemini 2.5, and it was a disaster for me. After experiencing the long-horizon and short-horizon styles, I did not want to use it. It made elementary mistakes, like leaving trailing dangling curly braces where they were not supposed to be. Then it would apologize profusely over and over, like a Canadian on steroids. (I'm a born and bred Canadian, I'm allowed to say that!)&lt;/p&gt;
&lt;p&gt;In contrast, Claude and Opus are consistently upbeat and positive, and the same can be said for Minimax-M.2 and GLM-4.7. That matters more than I expected. When something breaks and you are iterating quickly, a model that stays constructive keeps the whole loop feeling fun.&lt;/p&gt;
&lt;p&gt;On the other end, GPT-5.2 would just go ahead and do things without being overly effusive, then loop back to tell me what it did. That sounds fine on paper, but it left me feeling a bit clueless. I would wonder what it was doing and whether I could intercept it if it went off on the wrong tangent. I often could not, because I needed to wait until the end to learn what it decided to do.&lt;/p&gt;
&lt;p&gt;So yes, I care about correctness. But I also care about how a model behaves while it is getting to correctness. The journey matters because the journey is where you spend your time.&lt;/p&gt;
&lt;h2 id="enthusiasm-is-a-feature"&gt;Enthusiasm is a feature&lt;/h2&gt;&lt;p&gt;This ties nicely to &lt;a href="https://x.com/Grady_Booch/status/2013343499563999589"&gt;a tweet&lt;/a&gt; I saw from Grady Booch (@Grady_Booch):&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"The greatest value such tools have offered me is to reduce my cognitive load and automate various tedious tasks."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Here is the punchline:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"To serve as an enthusiastic and indefatigable, albeit very naive and often unreliable, pair programmer."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That enthusiasm and indefatigability, compared to a grumpy human, keeps the loop moving.&lt;/p&gt;
&lt;p&gt;My frustration pair coding with Gemini was not just the mistakes, it was the &lt;em&gt;emotional texture&lt;/em&gt; of the interaction. It would make mistakes and then apologize, repeatedly. After a while, you start optimizing your own behavior around the assistant's vibe, and that is not where you want your attention to go.&lt;/p&gt;
&lt;p&gt;A better pair programmer, human or AI, is relentlessly game for the next challenge. It affirms what you are trying to do, it corrects you when you are wrong, and it does not act like it is giving up. When the assistant stays constructive, the work stays fun.&lt;/p&gt;
&lt;h2 id="streaming-and-the-illusion-of-speed-the-harness-model"&gt;Streaming and the illusion of speed (the harness + model)&lt;/h2&gt;&lt;p&gt;Raw latency is one thing. What you &lt;em&gt;see&lt;/em&gt; while the model is working is another, and that is determined by the harness. In Cursor, you get a fast stream of tool calls and edits. You see the so-called thinking process. Something is clearly happening. With GLM-4.7 or Open Code in certain setups, you wait a long time with nothing streamed in—just a spinner or a blank state until the full response lands. Same model capability, same task, different harness, totally different experience. The harness that gives you a live trace makes the wait feel shorter and keeps you in the loop. The one that hides progress makes every request feel like a gamble. If you care about flow, streaming and visual feedback are not polish; they are table stakes, and they live in the harness.&lt;/p&gt;
&lt;h2 id="the-feel-is-also-vendor-lock-in"&gt;The "feel" is also vendor lock-in&lt;/h2&gt;&lt;p&gt;After enough hours with a single model, you start building muscle memory for its quirks. You learn how to phrase prompts so it does the right thing. You learn which mistakes to expect. You even learn its tone. That comfort is sticky.&lt;/p&gt;
&lt;p&gt;The sticky part is the problem. Getting used to a model's ergonomics is a form of vendor lock-in, and it is something I am determined to avoid.&lt;/p&gt;
&lt;p&gt;That is one reason I have been bouncing between models (apart from me hitting limits) to feel out the ragged frontier of model behavior. It is pretty revealing. You quickly learn that "best model" is not a single number. The model you want depends on whether you are scaffolding, refactoring, debugging, or doing the last-mile polish.&lt;/p&gt;
&lt;p&gt;If you want to keep your agency while using these tools, stay fluent across multiple feels. Otherwise you end up optimizing your workflow around one model's quirks and calling it productivity.&lt;/p&gt;
&lt;h2 id="a-more-pragmatic-way-to-think-about-model-choice"&gt;A more pragmatic way to think about model choice&lt;/h2&gt;&lt;p&gt;What I do now is less romantic than "find the best model". I think in terms of work phases and feedback loops.&lt;/p&gt;
&lt;p&gt;If I am scaffolding, I will happily take Opus-feel: longer-horizon autonomy and a big blob of output, because the cost of being wrong is usually low.&lt;/p&gt;
&lt;p&gt;If I am refactoring or debugging, I want Sonnet-feel: short-horizon iteration and tight supervision, because the cost of being wrong is a broken app and a bunch of time lost to verification.&lt;/p&gt;
&lt;p&gt;And if I keep hitting the same dumb failures, I try to fix my harness before I try to fix my model. Add the smallest test that fails fast, make it runnable by the agent, and suddenly the whole system behaves better. Cypress reloading the page and clicking one button did more for my sanity than another week of model hopping.&lt;/p&gt;
&lt;p&gt;At a systems level, I want a workflow where models are swappable components. In practice that means traces you can read, tests you can run, and a loop that tells you quickly when the agent broke something.&lt;/p&gt;
</content></entry><entry><title>How to build self-improving coding agents - Part 3</title><link href="https://ericmjl.github.io/blog/2026/1/19/how-to-build-self-improving-coding-agents-part-3/" rel="alternate"/><updated>2026-01-19T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:f1f0bd53-39f6-3f87-96d4-03384d798a21</id><content type="html">&lt;p&gt;In &lt;a href="../../17/how-to-build-self-improving-coding-agents-part-1/"&gt;part 1&lt;/a&gt;, I covered &lt;code&gt;AGENTS.md&lt;/code&gt; as repo memory.&lt;/p&gt;
&lt;p&gt;In &lt;a href="../../18/how-to-build-self-improving-coding-agents-part-2/"&gt;part 2&lt;/a&gt;, I covered skills as reusable playbooks.&lt;/p&gt;
&lt;p&gt;This post is about turning those two ideas into something you can run as a practice.&lt;/p&gt;
&lt;h2 id="the-maturity-model"&gt;The maturity model&lt;/h2&gt;&lt;p&gt;Once you have both repo memory and skills, you can think about how the practice evolves over time.&lt;/p&gt;
&lt;h3 id="stage-0-ad-hoc-prompting"&gt;Stage 0: Ad hoc prompting&lt;/h3&gt;&lt;p&gt;You keep re-explaining the same things in chat. It works, but it does not compound.&lt;/p&gt;
&lt;h3 id="stage-1-repo-local-memory"&gt;Stage 1: Repo-local memory&lt;/h3&gt;&lt;p&gt;You add repository-specific guardrails and a code map.&lt;/p&gt;
&lt;p&gt;This is where &lt;code&gt;AGENTS.md&lt;/code&gt; shines.&lt;/p&gt;
&lt;h3 id="stage-2-global-personal-skills"&gt;Stage 2: Global personal skills&lt;/h3&gt;&lt;p&gt;Once a workflow repeats across repos, you promote it into a global skill on your machine.&lt;/p&gt;
&lt;p&gt;If you want a concrete bootstrap set, here is what I would install globally:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/blob/main/skills/skill-creator/"&gt;&lt;code&gt;skill-creator&lt;/code&gt;&lt;/a&gt;: lowers the activation energy for making new skills.&lt;/li&gt;
&lt;li&gt;an installer and updater for skills, for example &lt;a href="https://github.com/numman-ali/openskills"&gt;&lt;code&gt;openskills&lt;/code&gt;&lt;/a&gt;: makes distribution and updates less annoying.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ericmjl/skills/tree/main/skills/agents-md-improver"&gt;&lt;code&gt;agents-md-improver&lt;/code&gt;&lt;/a&gt;: keeps the repo map current without you thinking about it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="stage-3-shared-skills"&gt;Stage 3: Shared skills&lt;/h3&gt;&lt;p&gt;If a workflow repeats across a team, it belongs in a shared location with a clear install path.&lt;/p&gt;
&lt;p&gt;I do not think you should start here. Start repo-local, then promote only when you feel the pain twice.&lt;/p&gt;
&lt;p&gt;Promotion decisions come from paying attention to what the agent actually does in practice.&lt;/p&gt;
&lt;h2 id="watch-traces-then-distill-constraints"&gt;Watch traces, then distill constraints&lt;/h2&gt;&lt;p&gt;If you work with agents long enough, you start to notice the model’s default moves.&lt;/p&gt;
&lt;p&gt;When I see an agent repeatedly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;taking an overcomplicated path&lt;/li&gt;
&lt;li&gt;missing a file I know is relevant&lt;/li&gt;
&lt;li&gt;applying a global refactor when a surgical fix is needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I treat that as a signal.&lt;/p&gt;
&lt;p&gt;Then I decide what kind of fix it is.&lt;/p&gt;
&lt;p&gt;If it is a repo invariant, a navigation hint, or a local norm, it belongs in &lt;code&gt;AGENTS.md&lt;/code&gt;. That is the always-on context for how work should happen in this repo.&lt;/p&gt;
&lt;p&gt;If it is a repeatable procedure with a clear output contract, it belongs in a skill.&lt;/p&gt;
&lt;p&gt;Sometimes the procedure is repo-specific. In that case I keep it as a repo-local skill. If I feel the pain twice in another repo, I promote it into a global skill.&lt;/p&gt;
&lt;p&gt;This is how you get operational learning without pretending the model is learning.&lt;/p&gt;
&lt;p&gt;Underneath, a lot of this comes down to writing instructions in a way that can be executed.&lt;/p&gt;
&lt;h2 id="markdown-is-becoming-executable"&gt;Markdown is becoming executable&lt;/h2&gt;&lt;p&gt;One reason this whole approach works is that the agent can execute what you write.&lt;/p&gt;
&lt;p&gt;When an LLM can execute tool calls, Markdown becomes an executable language.&lt;/p&gt;
&lt;p&gt;Skills fit this pattern. A &lt;code&gt;SKILL.md&lt;/code&gt; is just a structured instruction sheet, but it is also runnable in the sense that the agent can turn it into searches, file reads, edits, and command execution.&lt;/p&gt;
&lt;p&gt;The other trick is that skills are loaded on demand. The agent reads a short description first, then loads the full instructions only when it needs them.&lt;/p&gt;
&lt;p&gt;You can write a precise plan in plain language, and the agent can turn it into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;searches&lt;/li&gt;
&lt;li&gt;file reads&lt;/li&gt;
&lt;li&gt;surgical edits&lt;/li&gt;
&lt;li&gt;test runs&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is not magic. It still depends on linguistic precision. But the ergonomics shift. You can describe a workflow at the level you actually think about it, then let the agent do the clerical work.&lt;/p&gt;
&lt;p&gt;This is also why I like the runbook analogy, even with the caveats.&lt;/p&gt;
&lt;h2 id="when-to-update-agents-md-vs-create-a-skill"&gt;When to update &lt;code&gt;AGENTS.md&lt;/code&gt; vs create a skill&lt;/h2&gt;&lt;p&gt;Skills tell an agent how to do something.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; tells an agent how this repo works, and what rules it must follow while doing anything at all.&lt;/p&gt;
&lt;p&gt;Here is how I decide.&lt;/p&gt;
&lt;p&gt;Update &lt;code&gt;AGENTS.md&lt;/code&gt; when the instruction is specific to the repo:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;navigation help: where things live, what files matter, what to ignore&lt;/li&gt;
&lt;li&gt;local norms: build commands, test commands, environment rules, style constraints&lt;/li&gt;
&lt;li&gt;guardrails: what not to do in this repo&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Create a skill when the workflow is reusable, or when you want a named, on-demand playbook:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a multi-step procedure you want to invoke repeatedly&lt;/li&gt;
&lt;li&gt;a workflow that spans repos or products&lt;/li&gt;
&lt;li&gt;a task with a strict output contract (release announcements, status updates, summaries)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I am unsure, I start repo-local. If I feel the pain twice in another repo, I promote it into a global skill.&lt;/p&gt;
&lt;h2 id="the-meta-skill-is-metacognition"&gt;The meta skill is metacognition&lt;/h2&gt;&lt;p&gt;The most valuable “skill”, however, is not a file format. It is the habit of watching yourself work.&lt;/p&gt;
&lt;p&gt;I try to ask: what am I doing repeatedly that should be systematized?&lt;/p&gt;
&lt;p&gt;If the answer is “I keep re-explaining how this repo is organized”, that goes into &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If the answer is “I keep asking for the same kind of summary, debug sequence, or release note format”, that becomes a skill.&lt;/p&gt;
&lt;p&gt;Once you start doing this, you build a compounding loop. The agent handles more of the repeated work, and you spend more time on judgment and design.&lt;/p&gt;
&lt;p&gt;If this all sounds like more than coding, that is because it is.&lt;/p&gt;
&lt;h2 id="where-this-seems-to-be-going"&gt;Where this seems to be going&lt;/h2&gt;&lt;p&gt;I buy Simon Willison’s framing that these tools are general agents disguised as developer tools (&lt;a href="https://simonwillison.net/2026/Jan/12/claude-cowork/"&gt;Claude Cowork post&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Even if you start with coding, the moment an agent can run terminal commands and manipulate files, the surface area expands to “almost anything”, as long as you know how to steer it.&lt;/p&gt;
&lt;p&gt;That matches how I use coding agents.&lt;/p&gt;
&lt;p&gt;Yes, I use it for coding work. But I also use it for other intellectual work: ghostwriting blog posts (which I scrutinize heavily, because the review process is essential for me to own the content), writing release announcements, and turning messy notes into structured drafts.&lt;/p&gt;
&lt;p&gt;I have also heard Theo Brown make a similar point when talking about Claude Cowork (&lt;a href="https://www.youtube.com/watch?v=IcQEaopx90g"&gt;video&lt;/a&gt;). The details vary, but the pattern is the same: once you have a general agent, the label “coding tool” becomes more about marketing and UI than capability.&lt;/p&gt;
&lt;p&gt;So I am increasingly convinced that the long-term shape here is web-deployed agents with less scary branding.&lt;/p&gt;
&lt;p&gt;You will still want composable components for LLM workflows. But for day-to-day work, the most useful thing is an agent that can execute commands and apply changes, while carrying a growing set of skills and repository memory.&lt;/p&gt;
&lt;p&gt;That combination is what makes the agent feel less like a chat box and more like a teammate.&lt;/p&gt;
</content></entry><entry><title>How to build self-improving coding agents - Part 2</title><link href="https://ericmjl.github.io/blog/2026/1/18/how-to-build-self-improving-coding-agents-part-2/" rel="alternate"/><updated>2026-01-18T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:8caf7de9-77ba-3b15-be93-8de346553886</id><content type="html">&lt;p&gt;In &lt;a href="../../17/how-to-build-self-improving-coding-agents-part-1/"&gt;part 1&lt;/a&gt;, I focused on repo memory with &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In this post, I am switching to the other lever: skills.&lt;/p&gt;
&lt;h2 id="skills-are-prompt-compression"&gt;Skills are prompt compression&lt;/h2&gt;&lt;p&gt;Skills are the other half of the system.&lt;/p&gt;
&lt;p&gt;When a task repeats, I do not want to keep re-explaining the workflow. I want a playbook I can invoke.&lt;/p&gt;
&lt;h3 id="what-a-skill-is"&gt;What a skill is&lt;/h3&gt;&lt;p&gt;A skill is a folder with a &lt;code&gt;SKILL.md&lt;/code&gt; file.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;SKILL.md&lt;/code&gt; is the prompt. The bundled scripts and assets are the tool layer.&lt;/p&gt;
&lt;p&gt;A good skill makes three things explicit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;when to use it&lt;/li&gt;
&lt;li&gt;what steps to take&lt;/li&gt;
&lt;li&gt;what good output looks like&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want the spec, see &lt;a href="https://agentskills.io/home"&gt;Agent Skills&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Skills are best formed around jobs to be done: concrete, repeatable workflows rather than abstract capabilities. Think "debug a GitHub Actions failure" or "draft a release announcement," not "know about CI" or "write good prose." When the job is clear, the skill has a natural boundary and a clear trigger. When it is vague, the skill is hard to invoke and hard to improve.&lt;/p&gt;
&lt;p&gt;A wrong framing is "skills for tools." Skills get invoked in the loop of trying to accomplish a job, not in the context of trying to use a tool. The tool is a means; the job is why you reach for it. If you design a skill around a tool, you end up with something the agent has to remember to use. If you design it around a job, the agent reaches for it when the job shows up.&lt;/p&gt;
&lt;h3 id="examples"&gt;Examples&lt;/h3&gt;&lt;p&gt;A GitHub debugging skill is the obvious starting point. CI failures are repetitive and usually want the same sequence: identify failing jobs, pull logs, inspect diffs, reproduce locally, then patch.&lt;/p&gt;
&lt;p&gt;A second example is a release announcement skill.&lt;/p&gt;
&lt;p&gt;The motivation here was not abstract. I was spending a good half hour each release just trying to compose the announcement, and I did not want to do that anymore.&lt;/p&gt;
&lt;p&gt;The output contract was also specific. I wanted release announcements that are copy-pasteable into Microsoft Teams, with emojis, but otherwise minimal formatting because Teams formatting is inconsistent.&lt;/p&gt;
&lt;p&gt;A third example is more technical.&lt;/p&gt;
&lt;p&gt;At work I had a session with a coding agent to train an ML model inside a script. After that session, I had it write a report on what it learned and what changed. Then I turned that report writing into a skill.&lt;/p&gt;
&lt;p&gt;The report format was familiar to everyone on the team: Abstract, Introduction, Methods, Results, Discussion.&lt;/p&gt;
&lt;p&gt;The content came from real artifacts: stdout logs, metrics, code, config files, git diffs, and the agent’s own session history.&lt;/p&gt;
&lt;p&gt;A fourth example is about tacit domain expertise.&lt;/p&gt;
&lt;p&gt;A teammate of mine created a skill that encoded her implicit knowledge from years of debugging chromatography traces. The point was not that the agent suddenly became a scientist. The point was that her debugging procedure became explicit and reusable.&lt;/p&gt;
&lt;h3 id="skill-creation-and-iteration"&gt;Skill creation and iteration&lt;/h3&gt;&lt;p&gt;I now like skills because they are easy to iterate on. I used to be more skeptical, and I still think MCP servers have a cleaner distribution story, but my opinion has shifted as I have used skills more in real workflows (&lt;a href="https://ericmjl.github.io/blog/2025/10/20/exploring-skills-vs-mcp-servers/"&gt;Exploring Skills vs MCP Servers&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;For the release announcements, I fed my coding agent a few examples of what “good” looked like. I was using Anthropic’s &lt;a href="https://github.com/ericmjl/skills/blob/main/skills/skill-creator/"&gt;&lt;code&gt;skill-creator&lt;/code&gt;&lt;/a&gt; skill at the time, and those examples became part of the skill itself, stored as assets that the agent could reuse.&lt;/p&gt;
&lt;p&gt;This is a huge energy barrier reducer. It is much easier to iterate on a Markdown-based skill than it is to start from scratch with “write me a Python script that does X”. You can still add scripts inside a skill when you need determinism, but the interface is the Markdown.&lt;/p&gt;
&lt;p&gt;The other half is the feedback loop. When I edit the generated release announcement, I feed the revised version back to the agent and tell it to update the skill with the new example. That way the skill evolves as my taste evolves.&lt;/p&gt;
&lt;p&gt;This is also a way to share. A skill is reviewable. I can open a PR and let collaborators comment on both the output and the process that produced it.&lt;/p&gt;
&lt;p&gt;In the chromatography example, using &lt;a href="https://github.com/ericmjl/skills/blob/main/skills/skill-creator/"&gt;&lt;code&gt;skill-creator&lt;/code&gt;&lt;/a&gt; to generate the first draft mattered for another reason too. English is not my teammate’s first language. The structure makes it much easier to get from “I know what I do” to “here is the procedure an agent can follow”.&lt;/p&gt;
&lt;h3 id="distribution-and-updates"&gt;Distribution and updates&lt;/h3&gt;&lt;p&gt;This is where skills feel less mature than MCP servers.&lt;/p&gt;
&lt;p&gt;An MCP server has a clean distribution story. You can &lt;code&gt;pip install&lt;/code&gt; it, configure auth once, and you get a centrally versioned bundle of prompts and tools. Updating is a normal package update.&lt;/p&gt;
&lt;p&gt;Skills still involve moving folders between machines and repos, and remembering where each harness expects skills to live.&lt;/p&gt;
&lt;p&gt;I originally ended up writing a &lt;a href="https://github.com/ericmjl/skills/tree/main/skills/skill-installer"&gt;&lt;code&gt;skill-installer&lt;/code&gt;&lt;/a&gt; skill. It is the same move as &lt;a href="https://github.com/ericmjl/skills/blob/main/skills/skill-creator/"&gt;&lt;code&gt;skill-creator&lt;/code&gt;&lt;/a&gt;, but for distribution and updates.&lt;/p&gt;
&lt;p&gt;When I say “install this skill” or “update this skill from this URL”, the agent needs to ask two key questions if I have not already specified them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;is this repo-local or machine-global?&lt;/li&gt;
&lt;li&gt;which harnesses should discover it?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then it does the boring part consistently.&lt;/p&gt;
&lt;p&gt;Update: it looks like &lt;a href="https://github.com/numman-ali/openskills"&gt;&lt;code&gt;openskills&lt;/code&gt;&lt;/a&gt; now solves most of what I wanted here, and it does it more deterministically. It is a CLI that installs skill folders from GitHub or local paths, tracks their sources for updates, and can target multiple install locations.&lt;/p&gt;
&lt;p&gt;OpenSkills has a "universal" mode that installs to &lt;code&gt;.agent/skills&lt;/code&gt; (repo) and &lt;code&gt;~/.agent/skills&lt;/code&gt; (machine).&lt;/p&gt;
&lt;p&gt;The caveat is that &lt;code&gt;.agent/skills&lt;/code&gt; is not a universal discovery standard across harnesses. Some tools look in &lt;code&gt;.claude/skills&lt;/code&gt;, &lt;code&gt;.github/skills&lt;/code&gt;, &lt;code&gt;.opencode&lt;/code&gt;, or other locations. So OpenSkills helps with deterministic installs and updates, but you still need to know what your harness will actually read.&lt;/p&gt;
&lt;p&gt;I expect this to converge soon.&lt;/p&gt;
&lt;p&gt;At this point you have both memory and playbooks. The question becomes how you decide what to invest in next.&lt;/p&gt;
&lt;h2 id="coming-next"&gt;Coming next&lt;/h2&gt;&lt;p&gt;Part 3 covers the operating model.&lt;/p&gt;
&lt;p&gt;It lays out a maturity model, a concrete bootstrap set of skills to install globally, and a decision rule for when to update &lt;code&gt;AGENTS.md&lt;/code&gt; versus when to create a skill.&lt;/p&gt;
&lt;p&gt;&lt;a href="../../19/how-to-build-self-improving-coding-agents-part-3/"&gt;How to build self-improving coding agents - Part 3&lt;/a&gt;&lt;/p&gt;
</content></entry><entry><title>How to build self-improving coding agents - Part 1</title><link href="https://ericmjl.github.io/blog/2026/1/17/how-to-build-self-improving-coding-agents-part-1/" rel="alternate"/><updated>2026-01-17T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:f5c71a26-6381-3980-aa4e-20d1deeb4eb3</id><content type="html">&lt;p&gt;I want my coding agents to get better every week.&lt;/p&gt;
&lt;p&gt;Not in the abstract “the models are improving” sense. I mean it in the operational sense: if an agent makes a mistake, or takes a path I would not take, I want that feedback to stick. If I have to repeat the same preference every session, I am not using an agent. I am babysitting a very fast intern.&lt;/p&gt;
&lt;p&gt;The trick is that the model weights are not changing mid-week. So if you want “self-improvement”, you need to change the environment the agent works inside.&lt;/p&gt;
&lt;p&gt;I have found two levers that compound:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; as repository memory&lt;/li&gt;
&lt;li&gt;skills as reusable playbooks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post is a longer “source of truth” version. My intent is to later break it into smaller blog entries, and also rework it into chapters for my data science bootstrap notes.&lt;/p&gt;
&lt;h2 id="where-improvement-comes-from"&gt;Where improvement comes from&lt;/h2&gt;&lt;p&gt;The UX I am after is simple: I stop repeating myself. I stop doing the same end-of-day cleanup, writing the same reminders, re-explaining where files live. The agent starts each session closer to how I want it to work.&lt;/p&gt;
&lt;p&gt;If the model weights are not changing mid-week, improvement has to come from the environment you wrap around the agent.&lt;/p&gt;
&lt;p&gt;For me that environment has two pieces:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;durable repository memory (&lt;code&gt;AGENTS.md&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;reusable playbooks (skills)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Once you have those two, you can treat “agent improvement” like runbooks plus postmortems.&lt;/p&gt;
&lt;p&gt;The analogy is imperfect, because this is not documentation for humans. The loop is the same though: write down the repeatable steps, then write down what surprised you and what you will do differently next time.&lt;/p&gt;
&lt;p&gt;The difference is that natural language can turn into tool calls. When you write things down precisely, the agent can execute them.&lt;/p&gt;
&lt;p&gt;I usually start with &lt;code&gt;AGENTS.md&lt;/code&gt;, because it cuts down exploration immediately.&lt;/p&gt;
&lt;h2 id="agents-md-as-repository-memory"&gt;&lt;code&gt;AGENTS.md&lt;/code&gt; as repository memory&lt;/h2&gt;&lt;p&gt;If you have not run into the &lt;code&gt;AGENTS.md&lt;/code&gt; convention before, see &lt;a href="https://agents.md/"&gt;agents.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To be effective, &lt;code&gt;AGENTS.md&lt;/code&gt; needs to do two things for the agent.&lt;/p&gt;
&lt;p&gt;First, it needs to make the agent fast at navigating the repo so it can get to the right files with minimal wandering. A code map is a straightforward way to do that.&lt;/p&gt;
&lt;p&gt;Second, it needs to encode the local ways of working in this repo so the agent stops repeating the same mistakes. That is where corrections and norms live.&lt;/p&gt;
&lt;p&gt;This is the loop I want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I observe a mismatch.&lt;/li&gt;
&lt;li&gt;I tell the agent what must be true.&lt;/li&gt;
&lt;li&gt;The agent writes the correction into &lt;code&gt;AGENTS.md&lt;/code&gt; (or a repo-local skill).&lt;/li&gt;
&lt;li&gt;The agent reads it next time.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="fast-navigation-to-the-right-files"&gt;Fast navigation to the right files&lt;/h3&gt;&lt;p&gt;In the ideal state, the agent gets to the right files quickly.&lt;/p&gt;
&lt;p&gt;A code map is the simplest way I know to make that happen. It does not have to be perfect. It can be slightly stale and still be useful.&lt;/p&gt;
&lt;p&gt;I have seen this pay off in a very practical way. In my &lt;code&gt;canvas-chat&lt;/code&gt; codebase, having a map of the repo let the agent one-shot an obscure spot where events were emitted for node rendering. Without a map, the agent previously needed 5 to 6 &lt;code&gt;rg&lt;/code&gt; searches, just to find the right neighborhood of the code.&lt;/p&gt;
&lt;p&gt;The difference is small in absolute time, something like 40 seconds versus 2 seconds. But it changes the feel of the collaboration. The agent spends less time wandering, and I spend less time steering.&lt;/p&gt;
&lt;h3 id="close-the-loop-when-the-map-is-stale"&gt;Close the loop when the map is stale&lt;/h3&gt;&lt;p&gt;There is one extra move that makes this feel self-correcting: When the agent notices that the code map looks stale, it should update the code map.&lt;/p&gt;
&lt;p&gt;This is a subtle point. The map is not a static artifact. It is part of a feedback loop. When the agent’s exploration discovers a mismatch between the map and reality, that discovery should flow back into &lt;code&gt;AGENTS.md&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can encode this as an explicit instruction inside &lt;code&gt;AGENTS.md&lt;/code&gt;. You can also refresh on a schedule, like weekly, but the on-demand update is the part that makes the loop feel alive.&lt;/p&gt;
&lt;h3 id="corrections-that-become-durable-norms"&gt;Corrections that become durable norms&lt;/h3&gt;&lt;p&gt;The second job of &lt;code&gt;AGENTS.md&lt;/code&gt; is to hold repo-specific corrections to agents behaviour.&lt;/p&gt;
&lt;p&gt;These are the things you find yourself saying out loud.&lt;/p&gt;
&lt;p&gt;Two examples from my own work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run Python in the &lt;code&gt;pixi&lt;/code&gt; context. Use &lt;code&gt;pixi run python ...&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Do not cheat by modifying the tests to make them pass.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I say the first one because the agent will often try &lt;code&gt;python -c ...&lt;/code&gt; to quickly check something. In a &lt;code&gt;pixi&lt;/code&gt;-managed project, that fails if you do not have a global Python.&lt;/p&gt;
&lt;p&gt;I say the second one because changing tests to make them pass destroys the point of having tests.&lt;/p&gt;
&lt;p&gt;Once these rules are written down, the agent stops making you restate them. This is the simplest way I know to reduce repeated friction.&lt;/p&gt;
&lt;h2 id="a-starter-prompt-for-generating-agents.md`"&gt;A starter prompt for generating &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;I have found it useful to bootstrap &lt;code&gt;AGENTS.md&lt;/code&gt; with a one-time deep dive.&lt;/p&gt;
&lt;p&gt;Here is a prompt I use as a starting point. It is intentionally repo-specific.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;You are a coding agent. Read through this repository and create an `AGENTS.md` file at the repo root.

Requirements:
- Include a short codebase map that helps an agent find files quickly.
- Focus on entry points, directory roles, naming conventions, configuration wiring, and test locations.
- Add a section called &amp;quot;Local norms&amp;quot; with repo-specific rules you infer from the code and tooling.
- Add a section called &amp;quot;Self-correction&amp;quot; with two explicit instructions:
  - If the code map is discovered to be stale, update it.
  - If the user gives a correction about how work should be done in this repo, add it to &amp;quot;Local norms&amp;quot; (or another clearly labeled section) so future sessions inherit it.

Process:
- Use search and targeted file reads, do not read every file.
- Prefer `rg` searches to find entry points and configs.
- Prefer high-signal files: `README`, `pyproject.toml`, `package.json`, `Makefile`, `opencode.json`, `.github/workflows`, and top-level `src` or `app` directories.

Output:
- Write the final `AGENTS.md` contents in Markdown.
- Keep it concise. Optimize for navigation and correctness.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you want, you can go further and add a cadence rule like “refresh weekly”, but I would keep it lightweight. The goal is compounding value, not bureaucracy.&lt;/p&gt;
&lt;p&gt;Once &lt;code&gt;AGENTS.md&lt;/code&gt; exists, skills are the second lever.&lt;/p&gt;
&lt;h2 id="coming-next"&gt;Coming next&lt;/h2&gt;&lt;p&gt;Part 2 is about skills as reusable playbooks.&lt;/p&gt;
&lt;p&gt;It covers what a skill is, several examples from coding and scientific work, and why I ended up writing a &lt;code&gt;skill-installer&lt;/code&gt; skill to deal with the current distribution story.&lt;/p&gt;
&lt;p&gt;&lt;a href="../../18/how-to-build-self-improving-coding-agents-part-2/"&gt;How to build self-improving coding agents - Part 2&lt;/a&gt;&lt;/p&gt;
</content></entry><entry><title>How I fixed a browser selection bug with sequence alignment algorithms</title><link href="https://ericmjl.github.io/blog/2026/1/6/how-i-fixed-a-browser-selection-bug-with-sequence-alignment-algorithms/" rel="alternate"/><updated>2026-01-06T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:8ff1d33b-f8e0-3e00-bb6e-75771bc021b1</id><content type="html">&lt;p&gt;I ran into a frustrating bug this week in &lt;a href="https://github.com/ericmjl/canvas-chat"&gt;canvas-chat&lt;/a&gt;, my experimental canvas-based chat interface &lt;a href="../../../../2025/12/31/canvas-chat-a-visual-interface-for-thinking-with-llms/"&gt;I built at the end of last year&lt;/a&gt;. The bug seemed simple on the surface: when users selected text from a rendered markdown table and clicked to highlight it, the highlighting would sometimes stop partway through, or highlight the wrong characters entirely.&lt;/p&gt;
&lt;p&gt;What started as a "quick fix" turned into a journey through several failed approaches before I remembered an algorithm from my undergraduate bioinformatics days. Sometimes the best solution to a problem comes from an unexpected domain.&lt;/p&gt;
&lt;h2 id="the-problem-browser-selections-are-messy"&gt;The problem: Browser selections are messy&lt;/h2&gt;&lt;p&gt;Canvas-chat has a feature where you can select text from an AI response, and the app creates a "highlight" node that links back to the source. When you click on the highlight, the corresponding text in the source gets wrapped in a &lt;code&gt;&amp;lt;mark&amp;gt;&lt;/code&gt; tag.&lt;/p&gt;
&lt;p&gt;This worked fine for simple paragraphs. But when I tried it on tables containing KaTeX-rendered math, things went wrong:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What I expected to highlight:&lt;/strong&gt; &lt;mark&gt;66.00 (0.18±0.58)&lt;/mark&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What actually got highlighted:&lt;/strong&gt; &lt;mark&gt;66.00 ( 0.18&lt;/mark&gt;±0.58)&lt;/p&gt;
&lt;p&gt;The highlighting was off by more than a few characters, and would stop before the end of my selection. In some cases, it would highlight completely wrong sections.&lt;/p&gt;
&lt;h2 id="digging-into-the-root-cause"&gt;Digging into the root cause&lt;/h2&gt;&lt;p&gt;The problem came from how KaTeX renders math and how browsers handle text selection.&lt;/p&gt;
&lt;p&gt;KaTeX renders math with multiple text representations:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;katex&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cm"&gt;&amp;lt;!-- MathML for accessibility/screen readers --&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;katex-mathml&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;math&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;mn&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;0.13&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;mn&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;±&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;mo&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;math&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="cm"&gt;&amp;lt;!-- Visual HTML for display --&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;katex-html&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mord&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;0.13&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt; &lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;mord&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;±&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;span&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When you select text that spans across KaTeX-rendered content, &lt;code&gt;selection.toString()&lt;/code&gt; gives you something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;"66.00 (
0.13
±
0.13±0.58)"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice the duplicated &lt;code&gt;0.13&lt;/code&gt; and the random newlines? The browser included text from both the MathML (for accessibility) and the visual spans. Add in tabs between table cells and inconsistent spacing around operators, and you have a string that looks nothing like the clean HTML text content.&lt;/p&gt;
&lt;h2 id="first-attempt-normalization-layers"&gt;First attempt: Normalization layers&lt;/h2&gt;&lt;p&gt;My initial approach was to normalize both strings (the user's selection and the HTML text) before matching:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Collapse all whitespace to single spaces&lt;/li&gt;
&lt;li&gt;Remove KaTeX duplication patterns (like &lt;code&gt;0.13 ± 0.13±&lt;/code&gt; → &lt;code&gt;0.13±&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Normalize spacing around &lt;code&gt;±&lt;/code&gt; operators&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Then find the match in the normalized strings, and map the positions back to the original.&lt;/p&gt;
&lt;p&gt;This is where things got complicated. I needed to track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which positions in the normalized string corresponded to which positions in the original&lt;/li&gt;
&lt;li&gt;How to reverse the mapping after finding a match&lt;/li&gt;
&lt;li&gt;How to handle characters that got removed entirely during normalization&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The code became a tangled mess of position arrays and off-by-one bugs. Here's a simplified version of what it looked like:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;// Build mapping from normalized to original positions&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;normalizedToOriginal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;inWhitespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;leadingTrimmed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;fullText&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;inWhitespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;leadingTrimmed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;normalizedToOriginal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;inWhitespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;leadingTrimmed&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;inWhitespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nx"&gt;normalizedToOriginal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Then also account for math spacing normalization...&lt;/span&gt;
&lt;span class="c1"&gt;// And KaTeX deduplication...&lt;/span&gt;
&lt;span class="c1"&gt;// Each layer compounds the position mapping complexity&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The position mapping kept breaking. I'd fix one case only to break another. &lt;strong&gt;I was trying to maintain a bijection between two strings that had been transformed through multiple non-invertible operations.&lt;/strong&gt; It wasn't going to work.&lt;/p&gt;
&lt;h2 id="the-insight-this-is-a-sequence-alignment-problem"&gt;The insight: This is a sequence alignment problem&lt;/h2&gt;&lt;p&gt;After banging my head against the normalization approach for a while, I took a step back. What was I actually trying to do?&lt;/p&gt;
&lt;p&gt;I had two strings:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The user's selection (messy, with artifacts)&lt;/li&gt;
&lt;li&gt;The HTML text content (clean)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I needed to find where the user's selection "matched" in the HTML text, tolerating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Insertions (extra whitespace, duplicated characters in the selection)&lt;/li&gt;
&lt;li&gt;Deletions (characters present in HTML but not in selection)&lt;/li&gt;
&lt;li&gt;Mismatches (different whitespace characters)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This is exactly what sequence alignment algorithms are designed for. In bioinformatics, we use these algorithms to compare DNA or protein sequences that may have evolved with insertions, deletions, and mutations. The classic algorithm for finding the best local alignment between two sequences is &lt;strong&gt;Smith-Waterman&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I learned Smith-Waterman as an undergraduate, probably around 2008. I never thought I'd use it for web development.&lt;/p&gt;
&lt;h2 id="the-solution-align-the-beginning-and-end"&gt;The solution: Align the beginning and end&lt;/h2&gt;&lt;p&gt;I didn't need to align the entire selection - I just needed to find where it started and ended in the HTML text. So I:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Take the first ~20 characters of the user's selection and align them to find the &lt;strong&gt;start position&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Take the last ~20 characters, reverse both strings, align to find the &lt;strong&gt;end position&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Here's the core alignment function:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;alignStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;queryPrefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;queryPrefix&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// Reward for matching characters&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MISMATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Penalty for different characters&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GAP&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="c1"&gt;// Penalty for insertions/deletions&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WS_MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;// Softer reward for whitespace matches&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Build the scoring matrix&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;maxScore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;maxI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;maxJ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;qChar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;queryPrefix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tChar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;matchVal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qChar&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tChar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;matchVal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/\s/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qChar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WS_MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qChar&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/\s/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tChar&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;matchVal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;WS_MATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Any whitespace matches any whitespace&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;matchVal&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;MISMATCH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Local alignment can restart anywhere&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;matchVal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Diagonal: match/mismatch&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GAP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="c1"&gt;// Up: gap in target&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;GAP&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;// Left: gap in query&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;maxScore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;maxScore&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;maxI&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="nx"&gt;maxJ&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Traceback to find start position&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... (walk backwards from maxI, maxJ to find where alignment began)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The key insight is that Smith-Waterman's local alignment naturally handles all the messiness. Extra newlines in the selection? They're just gaps. Duplicated numbers? They align to the same position. Different whitespace characters? They all match each other.&lt;/p&gt;
&lt;h2 id="the-result"&gt;The result&lt;/h2&gt;&lt;p&gt;The new approach passes all the test cases that the normalization approach failed:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test: Simple word&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target: &lt;code&gt;"Hello world, this is a test."&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;"world"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Result: &lt;mark&gt;world&lt;/mark&gt; (positions 6-11)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Test: KaTeX duplication&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target: &lt;code&gt;"66.00 (0.18 ± 0.18±0.58)"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;"66.00 (\n0.18\n±\n0.18±0.58)"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Result: &lt;mark&gt;66.00 (0.18 ± 0.18±0.58)&lt;/mark&gt; (positions 0-25)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Test: Cross-block selection&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target: &lt;code&gt;"The Heading Some paragraph text here."&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Query: &lt;code&gt;"The Heading\n\nSome paragraph"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Result: &lt;mark&gt;The Heading Some paragraph&lt;/mark&gt; (positions 0-25)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-lesson-know-your-algorithms"&gt;The lesson: Know your algorithms&lt;/h2&gt;&lt;p&gt;I didn't invent anything new here. Smith-Waterman has been around since 1981. I just recognized that my web development problem was, at its core, a sequence alignment problem.&lt;/p&gt;
&lt;p&gt;This is why I think it's valuable to study algorithms and techniques from different domains, even if they seem unrelated to your day-to-day work. You never know when dynamic programming from bioinformatics will solve your JavaScript text highlighting bug.&lt;/p&gt;
&lt;p&gt;The normalization approach was trying to make two messy things identical before comparing them. The alignment approach embraced the messiness and asked: "Given that these are different, where do they best correspond?"&lt;/p&gt;
&lt;p&gt;That's a fundamentally different framing, and it's the framing that actually matched the problem.&lt;/p&gt;
&lt;p&gt;Interestingly, I couldn't find prior examples of using Smith-Waterman specifically for UI text highlighting or matching browser text selections to source HTML. The algorithm is well-established in bioinformatics for DNA and protein sequence alignment, and it appears in some fuzzy string matching contexts like spell-checking and record linkage. But applying it to handle the specific artifacts that browsers introduce when selecting text from rendered HTML with KaTeX, MathML, or complex table structures? That seems to be a new application. Sometimes the best solutions come from recognizing that your problem, despite appearing domain-specific, maps onto a well-solved problem from an entirely different field.&lt;/p&gt;
&lt;p&gt;One more note: I didn't write the JavaScript implementation myself. I directed Claude Opus 4.5 in &lt;a href="https://opencode.ai"&gt;OpenCode&lt;/a&gt; to write it for me. My contribution was recognizing that this was a sequence alignment problem and describing the approach - the actual code was generated by the AI. This is becoming my preferred way to work: I provide the domain insight and algorithmic direction, and the AI handles the implementation details.&lt;/p&gt;
&lt;h2 id="appendix-the-full-solution"&gt;Appendix: The full solution&lt;/h2&gt;&lt;p&gt;For those curious, the complete implementation is in the &lt;a href="https://github.com/ericmjl/canvas-chat/pull/97"&gt;pull request&lt;/a&gt;. The key functions are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;alignStart(queryPrefix, target)&lt;/code&gt; - Find where the query beginning matches&lt;/li&gt;
&lt;li&gt;&lt;code&gt;alignEnd(querySuffix, target)&lt;/code&gt; - Find where the query end matches (by reversing and aligning)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findMatchRegion(query, target)&lt;/code&gt; - Combine both to get the full match region&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The algorithm runs in O(mn) time where m and n are the lengths of the strings being aligned. For typical text selections (tens to hundreds of characters), this is instantaneous. And unlike the normalization approach, it's robust and correct!&lt;/p&gt;
</content></entry><entry><title>Canvas Chat: A Visual Interface for Thinking with LLMs</title><link href="https://ericmjl.github.io/blog/2025/12/31/canvas-chat-a-visual-interface-for-thinking-with-llms/" rel="alternate"/><updated>2025-12-31T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:d2aa2631-1832-38cd-8942-aab5690ab5ca</id><content type="html">&lt;p&gt;I've been mulling over this idea since last year January: A visual, nonlinear interface for LLM conversations—something like an infinite canvas where you could branch, merge, and see the shape of your thinking. It stayed in the "someday" pile because the implementation cost felt too high for a speculative side project; I wasn't skilled in browser technologies or anything UI-related.&lt;/p&gt;
&lt;p&gt;Then came the Christmas break ultralearning exercise I documented in &lt;a href="https://ericmjl.github.io/blog/2025/12/28/you-can-just-make-stuff-with-opencode-and-claude-opus-4-5/"&gt;my recent blog post about building with OpenCode and Claude Opus 4.5&lt;/a&gt;. Pressure-testing Opus 4.5 made me realize it was finally feasible to spend a day trying to make this work. I pushed Canvas Chat from idea to working prototype in about 24 hours of actual building time, and &lt;a href="https://ericmjl--canvas-chat-fastapi-app.modal.run/"&gt;then another 24 hrs to get it up on Modal&lt;/a&gt; and add in many, many refinements, each of which may have taken me multiple weeks. The final result is this:&lt;/p&gt;
&lt;p&gt;&lt;img src="canvas-chat-overview.webp" alt="Canvas Chat overview showing nodes connected in a directed graph"&gt;&lt;/p&gt;
&lt;p&gt;But before I explain what I built, let me explain &lt;em&gt;why&lt;/em&gt; I wanted it in the first place.&lt;/p&gt;
&lt;h2 id="the-job-to-be-done"&gt;The job to be done&lt;/h2&gt;&lt;p&gt;Clayton Christensen's &lt;a href="https://hbr.org/2016/09/know-your-customers-jobs-to-be-done"&gt;Jobs to Be Done&lt;/a&gt; framework asks: what job is the customer hiring this product to do? For Canvas Chat, the job isn't "chat with an LLM"—ChatGPT already does that fine. The job is: &lt;strong&gt;think through a complex problem where the exploration is nonlinear.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's the struggling moment. You're deep in a conversation with Claude or GPT, and you want to try a different framing of your question. But if you do, you'll lose the current thread. Or an LLM gives you a list of ten ideas, one catches your eye, and you want to drill into it—but the conversation keeps scrolling and you lose the overview. Or you've been exploring a problem across three separate chat sessions and now you need to synthesize, but you can't see them together.&lt;/p&gt;
&lt;p&gt;Linear chat actively works against this kind of thinking. It forces linear structure onto nonlinear exploration. You end up managing context in your head, copy-pasting between windows, losing track of which threads went where.&lt;/p&gt;
&lt;p&gt;Canvas Chat exists to solve that. When your thinking branches in multiple directions, it keeps all the threads visible and connected so you don't lose context and can synthesize across them.&lt;/p&gt;
&lt;h2 id="how-it-works"&gt;How it works&lt;/h2&gt;&lt;p&gt;Canvas Chat is an infinite canvas where conversations are nodes in a directed graph. You type a message, it appears as a node. The LLM's response appears as another node, connected by an edge. So far, standard. But then:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Branch from any node.&lt;/strong&gt; Click reply on any message, and your new message connects to that point, not the end of the conversation. The response branches off visually. Try two different prompts from the same starting point and see both branches side by side.&lt;/p&gt;
&lt;p&gt;&lt;img src="branch-from-node.webp" alt="Branching from a node to create parallel conversation threads"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Highlight and branch.&lt;/strong&gt; Select text within a node, and a tooltip appears. Type a follow-up question, and Canvas Chat creates a highlight node (showing the excerpt with a blockquote) plus your question, plus the LLM response. The original node stays intact. This works especially well when an LLM gives a list of ideas and you want to drill into one without losing the overview.&lt;/p&gt;
&lt;p&gt;&lt;img src="highlight-and-branch-tooltip.webp" alt="Tooltip appearing when text is selected within a node"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="highlight-and-branch-result.webp" alt="Result of highlight and branch showing the blockquote excerpt"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-select for merge context.&lt;/strong&gt; Cmd-click multiple nodes, then type. The new message connects to all selected nodes, and the LLM sees the full ancestry of every selected node. I use this to synthesize: select two branches that went in different directions, ask "What do these approaches have in common?" The context includes everything that led to both.&lt;/p&gt;
&lt;p&gt;&lt;img src="multi-select-merge.webp" alt="Multiple nodes selected for merge context synthesis"&gt;&lt;/p&gt;
&lt;h2 id="context-flows-through-the-graph"&gt;Context flows through the graph&lt;/h2&gt;&lt;p&gt;When you send a message, Canvas Chat walks the DAG backward from your selected node(s), collecting all ancestors. It sorts them by creation time and sends them to the LLM as conversation history. If you've selected multiple nodes (a merge), the context is the union of all their ancestors, deduplicated.&lt;/p&gt;
&lt;p&gt;The practical effect: the LLM always knows how you arrived at the current question, even if the path is nonlinear. Branch from a discussion about protein folding dynamics, ask a follow-up about computational costs, and the context includes the protein folding discussion. No manual copy-paste.&lt;/p&gt;
&lt;h2 id="matrix-evaluation"&gt;Matrix evaluation&lt;/h2&gt;&lt;p&gt;This feature came out of a specific struggling moment: evaluating many options against many criteria and losing track of which combinations I'd thought through.&lt;/p&gt;
&lt;p&gt;Select one or more nodes as context, type &lt;code&gt;/matrix &amp;lt;and then put additional instructions you're looking to fill out here&amp;gt;&lt;/code&gt;. Canvas Chat parses out the list items and shows a confirmation modal where you can remove items or swap rows/columns. Click create, and a matrix node appears.&lt;/p&gt;
&lt;p&gt;&lt;img src="matrix-evaluation-modal.webp" alt="Matrix evaluation modal for configuring rows and columns"&gt;&lt;/p&gt;
&lt;p&gt;Each cell has a "+" button. Click it and the LLM fills that cell, seeing the matrix context you provided, the row item, the column item, and the full DAG history from the source nodes. "Fill All" processes every empty cell sequentially.&lt;/p&gt;
&lt;p&gt;Click any filled cell to see the full text. "Pin to Canvas" extracts that evaluation into a standalone node, which you can then branch from. Say you're comparing business ideas against criteria, one cell says "strong market fit with enterprise customers," you want to dig into that—pin and branch.&lt;/p&gt;
&lt;p&gt;&lt;img src="matrix-evaluation-filled.webp" alt="Matrix evaluation with cells filled by the LLM"&gt;&lt;/p&gt;
&lt;h2 id="web-search-and-deep-research"&gt;Web search and deep research&lt;/h2&gt;&lt;p&gt;Canvas Chat integrates Exa's APIs for two slash commands:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/search &amp;lt;query&amp;gt;&lt;/code&gt; runs a neural search and creates a Search node with the query, plus Reference nodes for each result. Click "Fetch &amp;amp; Summarize" on any reference to grab the full page content and summarize it.&lt;/p&gt;
&lt;p&gt;&lt;img src="web-search-results.webp" alt="Web search results showing reference nodes"&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;/research &amp;lt;topic&amp;gt;&lt;/code&gt; kicks off Exa's Research API, which performs multi-step research with multiple queries. The results stream into a Research node with inline source citations.&lt;/p&gt;
&lt;p&gt;&lt;img src="deep-research-node.webp" alt="Deep research node with inline citations"&gt;&lt;/p&gt;
&lt;p&gt;If you have nodes selected when you run these commands, Canvas Chat uses an LLM to refine your query using the selected text as context. Highlight "CCNOT gate" and type &lt;code&gt;/search how does this work&lt;/code&gt;, and it rewrites the query to "how Toffoli gate CCNOT quantum computing works" before searching.&lt;/p&gt;
&lt;h2 id="local-first-and-multi-provider"&gt;Local-first and multi-provider&lt;/h2&gt;&lt;p&gt;&lt;img src="settings-api-keys.webp" alt="Settings panel showing API key configuration"&gt;&lt;/p&gt;
&lt;p&gt;All session data lives in IndexedDB. No server-side storage, no accounts. Export sessions as &lt;code&gt;.canvaschat&lt;/code&gt; JSON files. API keys live in localStorage and are sent with each request.&lt;/p&gt;
&lt;p&gt;The server is stateless: it proxies LLM calls via LiteLLM and handles the Exa integration, but never stores conversation data. You can deploy it yourself on Modal with a single command.&lt;/p&gt;
&lt;p&gt;Canvas Chat dynamically fetches available models from each provider when you enter an API key. OpenAI, Anthropic, Google (Gemini), Groq, GitHub Models, and local Ollama instances (when running on localhost) all work. Switch models mid-conversation to compare outputs.&lt;/p&gt;
&lt;h2 id="what-building-this-taught-me"&gt;What building this taught me&lt;/h2&gt;&lt;p&gt;This project reinforced something I wrote about in &lt;a href="../../28/you-can-just-make-stuff-with-opencode-and-claude-opus-4-5/"&gt;the "I don't code anymore, I build" post&lt;/a&gt;: I stayed in product builder brain throughout. I didn't have strong opinions about whether the JavaScript was idiomatic because I don't know what idiomatic JavaScript looks like. I just knew whether the feature worked.&lt;/p&gt;
&lt;p&gt;When something broke, I'd describe the symptoms and let Opus 4.5 debug in as much detail as I can manage. When I wanted a new interaction pattern, I'd describe what it should feel like and watch it materialize. The creative work — deciding what nonlinear chat should &lt;em&gt;be&lt;/em&gt; — remained human. The mechanical translation got delegated.&lt;/p&gt;
&lt;p&gt;Canvas Chat is the kind of project I wouldn't have attempted before because the implementation cost exceeded the payoff. Now it didn't.&lt;/p&gt;
&lt;h2 id="try-it"&gt;Try it&lt;/h2&gt;&lt;p&gt;Canvas Chat is open source. Run it locally:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/ericmjl/canvas-chat.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;canvas-chat
pixi&lt;span class="w"&gt; &lt;/span&gt;run&lt;span class="w"&gt; &lt;/span&gt;dev
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Add your API keys in settings and go. The deployed version runs on &lt;a href="https://ericmjl--canvas-chat-fastapi-app.modal.run/"&gt;Modal&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you try it, I want to hear what works and what doesn't! You can get in touch with me via &lt;a href="https://ericmjl--shortmail-run-app.modal.run/send/cce87ae9c1d7"&gt;Shortmail&lt;/a&gt;, or file an issue on the &lt;a href="https://github.com/ericmjl/canvas-chat"&gt;Github repo&lt;/a&gt;.&lt;/p&gt;
</content></entry><entry><title>You Can Just Make Stuff with OpenCode and Claude Opus 4.5</title><link href="https://ericmjl.github.io/blog/2025/12/28/you-can-just-make-stuff-with-opencode-and-claude-opus-4-5/" rel="alternate"/><updated>2025-12-28T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:c6c818be-320a-3218-b326-65fc288e1a41</id><content type="html">&lt;p&gt;&lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7408389557915799552?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7408389557915799552%2C7410474533469597697%29&amp;amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287410474533469597697%2Curn%3Ali%3Aactivity%3A7408389557915799552%29"&gt;Tommy Tang asked me&lt;/a&gt; about my opinions on OpenCode, so here's what I've learned after spending significant time with &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt; and Claude Opus 4.5.&lt;/p&gt;
&lt;h2 id="i-don-t-code-anymore-i-build"&gt;I don't code anymore, I build&lt;/h2&gt;&lt;p&gt;This is the punchline, so let me start with it. I've shifted from writing code to directing its creation. The change happened gradually, then all at once. I used to think about syntax, edge cases, and implementation details. Now I think about what I want to exist, describe it clearly, and watch it materialize.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.biblegateway.com/passage/?search=Genesis%201&amp;amp;version=NIV"&gt;Genesis 1:3&lt;/a&gt; describes this pattern at a cosmic scale:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"And God said, 'Let there be light,' and there was light."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Working with Claude Opus 4.5 through OpenCode feels like a microcosm of that creative act.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Eric said, "Let there be a feature," and there was the feature, in code.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm not claiming divinity here, just noting that the creative pattern of speaking things into existence has become surprisingly literal in my daily work.&lt;/p&gt;
&lt;h2 id="the-tools-opencode-and-claude-opus-4.5"&gt;The tools: OpenCode and Claude Opus 4.5&lt;/h2&gt;&lt;p&gt;Like &lt;a href="https://www.youtube.com/watch?v=4AyM_3SK31w&amp;amp;t=1263s"&gt;Theo Brown from t3.gg&lt;/a&gt;, I've settled on Claude Opus 4.5 as my primary model for coding tasks. It just knows what to do. I've stopped trying to micro-manage the model's actions because it handles most tasks autonomously and correctly. When I ask for a refactor, it refactors. When I describe a feature, it implements it. The gap between intention and execution has shrunk to almost nothing.&lt;/p&gt;
&lt;p&gt;Other models require more hand-holding. Opus 4.5 seems to have internalized enough software engineering patterns that I can trust it to make reasonable architectural decisions without constant course corrections. I can literally ask it to "do the docs, keep things up-to-date, and also give me a document that has an overview of code organization and architecture." It just goes to town autonomously. No step-by-step prompting, no breaking the task into smaller pieces. I describe the outcome I want and it figures out the path.&lt;/p&gt;
&lt;p&gt;The tooling layer matters too. &lt;a href="https://opencode.ai/"&gt;OpenCode&lt;/a&gt; orchestrates the AI coding in a way that feels natural. The tools it calls are always logical, the reasoning traces are transparent, and the execution flow makes sense. It shows a running list of modified files, giving me context about what's changing without running &lt;code&gt;git status&lt;/code&gt; constantly. Context compaction lets me stay in one long-running session without hitting token limits. I've thrown out the old playbook of "switch sessions when you approach the context window." Now I only switch when I want to do something entirely different.&lt;/p&gt;
&lt;p&gt;My setup: OpenCode with auto-updating, GitHub Copilot Pro as the LLM provider (routing to Opus 4.5), running inside a &lt;a href="https://github.com/tmux/tmux"&gt;tmux&lt;/a&gt; session for persistence. Each repo gets an AGENTS.md file where I encode my preferences and patterns - the model's training data for my specific context. Opus 4.5 actually respects what's in there, unlike some other models that seem to ignore custom instructions.&lt;/p&gt;
&lt;h2 id="ten-days-of-deliberate-practice"&gt;Ten days of deliberate practice&lt;/h2&gt;&lt;p&gt;I decided to pressure-test the "I build" claim over the holidays. Ten days, December 19-28, using OpenCode as my primary development interface. The goal: see how much I could actually ship.&lt;/p&gt;
&lt;p&gt;The answer surprised me. Across six repositories, I pushed over 150 commits spanning infrastructure work, documentation, greenfield apps, and maintenance. Here's what emerged:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A &lt;a href="https://ericmjl.github.io/2025-12-ski-trip-website/"&gt;ski trip coordination website&lt;/a&gt;&lt;/strong&gt; (59 commits). My family was heading to New Hampshire for a week. Normally I'd have used a shared Google Doc for the itinerary. Instead, I built a full website with recipe modals, restaurant links with Apple and Google Maps integration, a photo album with lightbox navigation, automatic thumbnail generation, and a hero video background. I updated it live during the trip - adding photos, adjusting the grocery list, swapping menu items. The implementation cost would have been absurd for a week-long trip before. Now the jazz and snazz was well worth the effort - my family actually enjoyed using it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A teaching clock app for my kids&lt;/strong&gt; (2 commits, but a complete app). An &lt;a href="https://ericmjl.github.io/teaching-clock/"&gt;analog clock trainer&lt;/a&gt; plus a &lt;a href="https://ericmjl.github.io/teaching-clock/puzzle.html"&gt;jigsaw puzzle game&lt;/a&gt; with difficulty levels and themes. Pure JavaScript and CSS - exactly the kind of project my decade-old "no JavaScript" rule would have blocked. The model wrote it; I directed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/pyjanitor-devs/pyjanitor"&gt;pyjanitor&lt;/a&gt; infrastructure&lt;/strong&gt; (40 commits). Currency symbol support for international formats. Automated patch releases on every merge. Test isolation fixes. And a major expansion of AGENTS.md into what I now think of as the repository's "agent constitution" - a document that tells AI assistants how to work within this specific codebase.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A new &lt;a href="https://github.com/conda-forge/staged-recipes/pull/31776"&gt;conda-forge package&lt;/a&gt;&lt;/strong&gt; for janitor-rs. The model handled the unfamiliar territory of Rust packaging and conda-forge recipe formats. I was the novice here; it was the guide. This role reversal keeps happening - when I set up PostHog analytics or migrated to GA4 on my website, the model walked me through each step, explained what I was doing and why, and waited for confirmation before proceeding. The expert-novice relationship flips depending on who knows more about the task at hand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A &lt;a href="../../27/how-i-themed-my-tmux-with-opencode-and-claude/"&gt;custom tmux status bar&lt;/a&gt;&lt;/strong&gt; with Nord colors, powerline arrows, and smooth color transitions. Pure aesthetic indulgence - the kind of project I'd never have prioritized before because the implementation cost exceeded the payoff. Now it didn't.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ericmjl/canvas-chat"&gt;Canvas Chat&lt;/a&gt;&lt;/strong&gt; (13 commits in 24 hours). A visual non-linear chat interface - think infinite canvas meets LLM conversation. Resizable nodes, trackpad gestures, streaming responses, web search via Exa, session management. FastAPI backend, vanilla JS frontend. Another "no JavaScript" rule violation, and another project that went from idea to working prototype in a single day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Smaller fixes across &lt;a href="https://github.com/ericmjl/llamabot"&gt;llamabot&lt;/a&gt;&lt;/strong&gt; (better error messages) and &lt;strong&gt;&lt;a href="https://github.com/ericmjl/website"&gt;my website&lt;/a&gt;&lt;/strong&gt; (PostHog analytics, GA4 migration, blog posts).&lt;/p&gt;
&lt;p&gt;The variety matters. This wasn't one type of project where I got lucky. It was infrastructure, documentation, greenfield consumer apps, packaging for an ecosystem I rarely touch, and routine maintenance. The "I build" claim held up across all of them.&lt;/p&gt;
&lt;h2 id="from-engineer-brain-to-product-builder-brain"&gt;From engineer brain to product builder brain&lt;/h2&gt;&lt;p&gt;Something shifted in how I think about these projects. Previously, I'd worry about &lt;em&gt;how&lt;/em&gt; a thing was built - the engineer brain obsessing over implementation details, code structure, idiomatic patterns. Now I've switched to &lt;em&gt;what&lt;/em&gt; was built, &lt;em&gt;why&lt;/em&gt; I want it built, and &lt;em&gt;does it get the job done&lt;/em&gt; - the product builder's brain.&lt;/p&gt;
&lt;p&gt;This is especially true for the ski website and Canvas Chat, both built with web technologies (HTML, JS, CSS) that I'm not deeply familiar with. Ironically, my unfamiliarity frees me from micro-managing the implementation. I don't have strong opinions about whether the JavaScript is idiomatic because I don't know what idiomatic JavaScript looks like. I just know whether the feature works.&lt;/p&gt;
&lt;p&gt;But there's a latent risk here. The code might not follow best practices - lots of duplication, poor separation of concerns, missing edge cases. So I fall back on &lt;em&gt;principles&lt;/em&gt; I picked up from years of Python: refactoring, documentation, testing. I stay at that level of nudging Opus 4.5 - "look for places to refactor," "document this module," "add tests for this functionality" - but I stay out of the nitty-gritty implementation. The principles transfer even when the language doesn't.&lt;/p&gt;
&lt;h2 id="how-my-review-process-changed"&gt;How my review process changed&lt;/h2&gt;&lt;p&gt;Here's something I didn't expect: I don't scrutinize the code as tightly as I used to during active development. Instead, I read the reasoning traces first. The model's chain of thought tells me whether my codebase is heading in the right direction. If the reasoning is coherent and addresses the right concerns, the code will reflect what I want. If the reasoning seems confused or takes weird detours, something's wrong and I need to dig deeper.&lt;/p&gt;
&lt;p&gt;This inverts the traditional development loop. I used to read code to understand what the computer would do. Now I read reasoning to understand what the model understood and decided. The code review happens afterward, and it's lighter because the reasoning already told me whether we're on track.&lt;/p&gt;
&lt;p&gt;When I want to catch issues that slipped through, I start a fresh session. A new context window acts like a fresh pair of eyes - the model hasn't been primed by the conversation that led to the current implementation, so it can spot inconsistencies that were invisible during the creative flow. This parallels the old advice about stepping away from code before reviewing it, except now the "stepping away" happens by instantiating a new session rather than waiting for my own brain to reset.&lt;/p&gt;
&lt;h2 id="unlearning-old-assumptions"&gt;Unlearning old assumptions&lt;/h2&gt;&lt;p&gt;&lt;a href="https://x.com/bcherny/status/2004626064187031831"&gt;Boris Cherny recently had a Twitter exchange with Andrej Karpathy&lt;/a&gt; that resonated with me. Boris observed that newer coworkers and even new grads who don't make assumptions about what the model can and can't do are often able to use it most effectively. They don't carry "legacy memories formed when using old models." Every month or two, models get better, and those of us who've been using them longest have to actively unlearn outdated limitations.&lt;/p&gt;
&lt;p&gt;I've caught myself doing this repeatedly. Back in grad school around 2015, I tried building &lt;a href="https://d3js.org/"&gt;d3.js&lt;/a&gt; visualizations and struggled to adjust to JavaScript's syntax coming from Python. I decided to focus on getting better at Python first and gave myself a "no JavaScript" rule wherever possible. That constraint made sense at the time. It makes no sense now. The model writes JavaScript just fine. My decade-old "no JavaScript" policy was a legacy memory holding me back from building things that would actually benefit from running in the browser.&lt;/p&gt;
&lt;p&gt;The mental work of re-adjusting expectations is real. I have to keep asking myself: would I have avoided this six months ago because the model couldn't handle it, or because I assumed it couldn't? The answer is increasingly the latter.&lt;/p&gt;
&lt;p&gt;There's a flip side to this unlearning, though. Working in JavaScript land forced me to learn the language of the web to achieve the same precision and fluency I have with Python. I found myself picking up patterns I'd avoided for years: the browser console for debugging, DOM element manipulation, CSS transitions I didn't know existed, the JS package ecosystem. The model writes the code, but I still need enough vocabulary to direct it well and recognize when something's off. Unlearning old constraints doesn't mean staying ignorant of new territory - it means finally having a reason to explore it.&lt;/p&gt;
&lt;h2 id="what-this-means"&gt;What this means&lt;/h2&gt;&lt;p&gt;The shift from "I code" to "I build" isn't just semantic. It reflects a genuine change in what I spend my attention on. Less time on syntax and implementation details. More time on architecture, requirements, and verification. The creative work remains human. The mechanical translation has been delegated.&lt;/p&gt;
&lt;p&gt;I'm still learning how to use this effectively. But the trajectory is clear: the gap between imagining software and having software continues to shrink.&lt;/p&gt;
</content></entry><entry><title>How I Themed My tmux with OpenCode + Claude (And When to Switch Models)</title><link href="https://ericmjl.github.io/blog/2025/12/27/how-i-themed-my-tmux-with-opencode-and-claude/" rel="alternate"/><updated>2025-12-27T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:9ed7b606-5f8b-3aef-8718-ba121e610e6e</id><content type="html">&lt;p&gt;I had a beautiful tmux status bar on my old laptop. Nord colors, powerline arrows, clean and minimal. The kind that makes you feel like a proper terminal power user.&lt;/p&gt;
&lt;p&gt;When I got a new machine back in April, I was too lazy to set up tmux properly. The sensible thing would have been to spend five minutes copying over my old config. Instead, eight months later, I finally spent an hour pair-programming the whole thing from scratch with &lt;a href="https://github.com/sst/opencode"&gt;OpenCode&lt;/a&gt; and Claude.&lt;/p&gt;
&lt;p&gt;Why? Honestly, I wanted to try out a new tool. The irony isn't lost on me.&lt;/p&gt;
&lt;h2 id="the-setup"&gt;The Setup&lt;/h2&gt;&lt;p&gt;OpenCode is a CLI tool that lets you interact with Claude directly from your terminal. Perfect for this kind of task: I'm already in the terminal configuring tmux, so having my AI pair programmer right there keeps the feedback loop tight. Describe what I want. See the change. Describe what's wrong, with precision. Iterate. No context switching to a browser.&lt;/p&gt;
&lt;p&gt;That tight loop is what let me stay in the creative headspace. I could say things like "I want the arrows to overlap like in this screenshot" or "the colors feel too muted, try the frost blue from Nord" without knowing the exact syntax. Claude translated my aesthetic intent into working config.&lt;/p&gt;
&lt;p&gt;The other superpower: model switching. OpenCode lets you flip between any models you have API keys for. For this session, I toggled between Claude Sonnet (fast, good for quick iterations) and Claude Opus (slower, but sharper for complex debugging). This turned out to be crucial.&lt;/p&gt;
&lt;h2 id="starting-with-research"&gt;Starting with Research&lt;/h2&gt;&lt;p&gt;First, I asked Sonnet to search online for tmux status bar customization. It pulled resources from the official tmux wiki and various tutorials, giving me a foundation: &lt;code&gt;status-left&lt;/code&gt;, &lt;code&gt;status-right&lt;/code&gt;, &lt;code&gt;window-status-format&lt;/code&gt;, color options, the basics.&lt;/p&gt;
&lt;p&gt;Armed with that, we dove in.&lt;/p&gt;
&lt;h2 id="first-attempt-with-a-custom-theme"&gt;First attempt with a custom theme&lt;/h2&gt;&lt;p&gt;Claude created a custom dark theme inspired by Catppuccin colors. Worked immediately:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;status-style&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;bg=#1e1e2e,fg=#cdd6f4&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;status-left&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#89b4fa,bold] #S #[fg=#a6e3a1]@ #H&amp;quot;&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;status-right&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#f9e2af]%a %b %d #[fg=#89b4fa]%H:%M&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Clean. Functional. Pretty. But I wanted more: those beautiful powerline arrows flowing between segments. That's when things got interesting.&lt;/p&gt;
&lt;h2 id="the-powerline-saga"&gt;The Powerline Saga&lt;/h2&gt;&lt;p&gt;Claude suggested &lt;code&gt;powerline-go&lt;/code&gt;, a Go-based powerline prompt generator. We installed it via Homebrew (not pip, since &lt;a href="https://ericmjl.github.io/blog/2024/8/16/its-time-to-try-out-pixi/"&gt;I keep my system Python-free&lt;/a&gt;):&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;brew&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;powerline-go
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Updated the tmux config to call powerline-go for the status bar. Reloaded. And... disaster. Instead of beautiful arrows, raw escape codes:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;[38;5;15m[48;5;4m ericmjl [38;5;4m[48;5;0m...
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The terminal was spitting out ANSI codes instead of interpreting them. We tried various fixes, but powerline-go simply wasn't designed for tmux status bars; it's meant for shell prompts. Back to square one.&lt;/p&gt;
&lt;h2 id="trying-the-tmux-powerline-plugin"&gt;Trying the tmux-powerline Plugin&lt;/h2&gt;&lt;p&gt;Next attempt: the actual &lt;code&gt;tmux-powerline&lt;/code&gt; plugin via TPM (Tmux Plugin Manager):&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/tmux-plugins/tpm&lt;span class="w"&gt; &lt;/span&gt;~/.tmux/plugins/tpm
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Added the plugin, pressed &lt;code&gt;C-b I&lt;/code&gt; to install, and... the status bar exploded with information. IP addresses, weather, load averages, hostname. Way too much. I asked Claude to simplify and switch to Nord colors.&lt;/p&gt;
&lt;p&gt;We created a custom theme at &lt;code&gt;~/.config/tmux-powerline/themes/nord.sh&lt;/code&gt;, updated the config, reloaded tmux. Nothing changed. The theme wasn't loading. Killed the server entirely. Restarted. Still the old crowded theme.&lt;/p&gt;
&lt;p&gt;This is where Sonnet started struggling. Same fixes over and over: reload the config, check the theme path, restart tmux. Loop after loop of suggestions that weren't working.&lt;/p&gt;
&lt;h2 id="the-model-switch-from-sonnet-to-opus"&gt;The model switch from Sonnet to Opus&lt;/h2&gt;&lt;p&gt;I noticed Sonnet spinning its wheels. Same suggestions, same non-results. Time to switch.&lt;/p&gt;
&lt;p&gt;The difference was immediate. Instead of repeating failed approaches, Opus stepped back and proposed something different entirely: ditch the plugin and go native. Tmux's built-in formatting is powerful enough to create powerline-style status bars without any plugins. We just needed the right Unicode characters and color transitions.&lt;/p&gt;
&lt;p&gt;This stuck with me: Sonnet is fantastic for speed and quick iterations, but when you're stuck in a loop, Opus brings the lateral thinking to break out.&lt;/p&gt;
&lt;h2 id="going-native-as-the-winning-approach"&gt;Going native as the winning approach&lt;/h2&gt;&lt;p&gt;Fresh start. Clean native tmux config. The key insight was understanding how powerline arrows actually work: the arrow character's foreground color matches the background of the segment it's coming from, and its background matches what it's going into.&lt;/p&gt;
&lt;p&gt;Here's the final status-left (session name with powerline arrow):&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;status-left&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#2e3440,bg=#5e81ac,bold]  #S #[fg=#5e81ac,bg=#2e3440]\ue0b0&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The window formats, with arrows on both sides so they flow into neighboring elements:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Inactive windows&lt;/span&gt;
setw&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;window-status-format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#2e3440,bg=#3b4252]\ue0b0#[fg=#d8dee9,bg=#3b4252] #I #W #[fg=#3b4252,bg=#2e3440]\ue0b0&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Active window (cyan highlight)&lt;/span&gt;
setw&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;window-status-current-format&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#2e3440,bg=#88c0d0]\ue0b0#[fg=#2e3440,bg=#88c0d0,bold] #I #W #[fg=#88c0d0,bg=#2e3440]\ue0b0&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And the right side (battery, date, time) using left-pointing arrows and a smooth Nord color gradient:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-g&lt;span class="w"&gt; &lt;/span&gt;status-right&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;#[fg=#a3be8c,bg=#2e3440]\ue0b2#[fg=#2e3440,bg=#a3be8c,bold] 󰁹 #(pmset -g batt | grep -o &amp;#39;[0-9]*%%&amp;#39; | head -1) #[fg=#5e81ac,bg=#a3be8c]\ue0b2#[fg=#d8dee9,bg=#5e81ac] %b %d #[fg=#88c0d0,bg=#5e81ac]\ue0b2#[fg=#2e3440,bg=#88c0d0,bold] %H:%M &amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="the-final-result"&gt;The Final Result&lt;/h2&gt;&lt;p&gt;After all that iteration, here's what my tmux status bar looks like:&lt;/p&gt;
&lt;div style="background: #2e3440; font-family: 'JetBrains Mono', 'Fira Code', monospace; padding: 8px 0; display: flex; flex-wrap: nowrap; justify-content: space-between; align-items: center; border-radius: 4px; overflow: hidden; min-width: 0;"&gt;
  &lt;div style="display: flex; flex-wrap: nowrap; align-items: center; height: 24px; flex-shrink: 0;"&gt;
    &lt;span style="background: #5e81ac; color: #2e3440; padding: 0 12px; font-weight: bold; height: 100%; display: flex; align-items: center; white-space: nowrap;"&gt;system-config&lt;/span&gt;
    &lt;div style="width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-left: 12px solid #5e81ac; flex-shrink: 0; position: relative; z-index: 2;"&gt;&lt;/div&gt;
    &lt;span style="background: #88c0d0; color: #2e3440; padding: 0 12px; font-weight: bold; height: 100%; display: flex; align-items: center; white-space: nowrap; margin-left: -12px; padding-left: 20px;"&gt;1 opencode&lt;/span&gt;
    &lt;div style="width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-left: 12px solid #88c0d0; flex-shrink: 0;"&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div style="display: flex; flex-wrap: nowrap; align-items: center; height: 24px; flex-shrink: 0;"&gt;
    &lt;div style="width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-right: 12px solid #a3be8c; flex-shrink: 0;"&gt;&lt;/div&gt;
    &lt;span style="background: #a3be8c; color: #2e3440; padding: 0 12px; font-weight: bold; height: 100%; display: flex; align-items: center; white-space: nowrap; padding-right: 20px;"&gt;🔋 100%&lt;/span&gt;
    &lt;div style="width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-right: 12px solid #5e81ac; flex-shrink: 0; margin-left: -12px; position: relative; z-index: 2;"&gt;&lt;/div&gt;
    &lt;span style="background: #5e81ac; color: #d8dee9; padding: 0 12px; height: 100%; display: flex; align-items: center; white-space: nowrap; padding-right: 20px;"&gt;Dec 23&lt;/span&gt;
    &lt;div style="width: 0; height: 0; border-top: 12px solid transparent; border-bottom: 12px solid transparent; border-right: 12px solid #88c0d0; flex-shrink: 0; margin-left: -12px; position: relative; z-index: 2;"&gt;&lt;/div&gt;
    &lt;span style="background: #88c0d0; color: #2e3440; padding: 0 12px; font-weight: bold; height: 100%; display: flex; align-items: center; white-space: nowrap;"&gt;06:05&lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Session name in frost blue on the left. Active window in cyan. Right side flows through battery (green), date (blue), and time (cyan). All connected by powerline arrows with smooth color transitions. &lt;em&gt;(I asked Claude to recreate the status bar in HTML so I wouldn't have to screenshot it for the blog.)&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="what-i-took-away"&gt;What I Took Away&lt;/h2&gt;&lt;p&gt;There's a growing conversation about AI-assisted programming: the tight feedback loops, model selection strategies, iterative workflows. I've written about some of these patterns myself. But this session crystallized something different.&lt;/p&gt;
&lt;p&gt;I can express my creativity on a computer screen more easily than ever before.&lt;/p&gt;
&lt;p&gt;I'm not a designer. CSS is foreign to me, hex color codes don't stick in my head, and tmux's formatting syntax is arcane. But I have taste. I know what looks good. Years of admiring beautiful terminals gave me a mental mood board. What I lacked was the technical fluency to make it real.&lt;/p&gt;
&lt;p&gt;AI bridged that gap. Throughout this session I worked like a designer: describing aesthetics, pointing at visual problems, directing iteration. "The arrows should overlap." "That cyan is too bright." "Make the battery segment green." Claude handled implementation. I stayed in the creative headspace.&lt;/p&gt;
&lt;p&gt;Iteration surfaces what you actually want.&lt;/p&gt;
&lt;p&gt;This surprised me. I didn't start with a complete vision, just a vague sense of "Nord colors, powerline arrows, clean and minimal." But each rapid cycle surfaced preferences I didn't know I had. The arrows need to overlap. The active window should pop more. The right side needs a color gradient. None of these were requirements I could have articulated upfront. They emerged through seeing and reacting.&lt;/p&gt;
&lt;p&gt;Bits and bytes have never been cheaper to produce. AI can generate config files, CSS, code, whatever. But aesthetics and judgment? Those remain expensive. The scarce resource isn't the implementation anymore. It's knowing what you want and recognizing when you've found it.&lt;/p&gt;
&lt;p&gt;AI doesn't replace that judgment. It amplifies it by removing the implementation friction that used to slow the creative loop down.&lt;/p&gt;
&lt;p&gt;The whole session took about an hour, failed attempts included. Without AI pair programming, I'd probably still be reading documentation. Instead, I have a beautiful terminal, and a new appreciation for what becomes possible when the gap between creative vision and technical implementation shrinks to nearly nothing.&lt;/p&gt;
</content></entry><entry><title>Two years of weekly blogging and what 2025 taught me</title><link href="https://ericmjl.github.io/blog/2025/12/25/two-years-of-weekly-blogging-and-what-2025-taught-me/" rel="alternate"/><updated>2025-12-25T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:af479e56-5fbc-334a-b7c8-6532edd8477b</id><content type="html">&lt;p&gt;Last year, I challenged myself to write one blog post per week,
and I hit 53 posts by the end of 2024.
This year, I doubled down on that commitment
and wrote 50 posts in 2025.
Including this one, it's 51,
bringing me to 104 blog posts over two years.&lt;/p&gt;
&lt;h2 id="the-year-of-coding-agents"&gt;The year of coding agents&lt;/h2&gt;&lt;p&gt;Looking at my 2025 posts,
one theme dominates: &lt;strong&gt;coding agents&lt;/strong&gt;.
I wrote extensively about how to work with AI coding assistants,
from teaching them with AGENTS.md files
to letting them work autonomously.
This reflected a shift in how I work day-to-day.&lt;/p&gt;
&lt;p&gt;Some highlights from this theme:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/"&gt;How to teach your coding agent with AGENTS.md&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/8/safe-ways-to-let-your-coding-agent-work-autonomously/"&gt;Safe ways to let your coding agent work autonomously&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/10/productive-patterns-for-agent-assisted-programming/"&gt;Productive Patterns for Agent-Assisted Programming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;How I Replaced 307 Lines of Agent Code with 4 Lines&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The shift from "AI as a tool" to "AI as a collaborator"
captures how my practice evolved this year.
I've gone from cautiously experimenting with Cursor
to having established patterns for multi-repository agent workflows.&lt;/p&gt;
&lt;h2 id="bayesian-methods-and-biological-applications"&gt;Bayesian methods and biological applications&lt;/h2&gt;&lt;p&gt;My work continued to inform my writing,
with several posts on applying Bayesian statistics to real lab problems.
The R2D2 prior posts were particularly satisfying to write
because I felt equipped with new theoretical knowledge that was directly applicable,
and I appreciated the mathematical aesthetics behind the approach:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;Bayesian Superiority Estimation with R2D2 Priors: A Practical Guide for Protein Screening&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/6/stop-guessing-at-priors-r2d2s-automated-approach-to-bayesian-modeling/"&gt;Stop guessing at priors: R2D2's automated approach to Bayesian modeling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/5/from-data-chaos-to-statistical-clarity-a-laboratory-transformation-story/"&gt;From data chaos to statistical clarity: A laboratory transformation story&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also explored the challenges of working with lab data,
including why preclinical experiments make ML challenging
and how to communicate effectively with lab scientists.&lt;/p&gt;
&lt;h2 id="tools-i-got-excited-about"&gt;Tools I got excited about&lt;/h2&gt;&lt;p&gt;Every year brings new tools that change how I work.
In 2025, two stood out.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://marimo.io/"&gt;Marimo&lt;/a&gt; is a reactive notebook tool
that I wrote about with enthusiasm,
and followed up with practical guidance on using coding agents to write Marimo notebooks.
The reactive execution model aligns well with how I think about data exploration.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://modal.com/"&gt;Modal&lt;/a&gt; is cloud computing that actually feels Pythonic.
My "Wow, Modal!" post captured the delight of finding infrastructure
that doesn't fight against my workflow.&lt;/p&gt;
&lt;h2 id="data-science-leadership-and-career"&gt;Data science leadership and career&lt;/h2&gt;&lt;p&gt;I continued writing about the human side of data science work,
including standardizing ways of working, communicating with lab scientists,
and navigating the biotech industry's ups and downs.
The year ended with
&lt;a href="https://ericmjl.github.io/blog/2025/12/17/the-selfish-reason-to-do-your-best-work/"&gt;The selfish reason to do your best work&lt;/a&gt;,
which synthesized lessons from a challenging year in biotech.&lt;/p&gt;
&lt;h2 id="looking-ahead-to-2026"&gt;Looking ahead to 2026&lt;/h2&gt;&lt;p&gt;After two years of writing almost weekly on whatever is on my mind,
I am adjusting my goals.
Next year, my attention shifts towards
(a) learning the fundamentals of quantum computing through an ultralearning project,
(b) writing more on data science leadership and career development to encourage colleagues navigating similar paths, and
(c) building out at least 10 experimental things with AI.
I am also dropping the goal of "one blog post per week" to four per month,
which brings me to a goal of 48 for 2026.
I am giving myself space to rest and strategically plan out writing going into 2026.&lt;/p&gt;
&lt;p&gt;Merry Christmas and a happy new year to all my readers!&lt;/p&gt;
&lt;h2 id="blog-posts-by-theme"&gt;Blog posts by theme&lt;/h2&gt;&lt;h3 id="biology-chemistry"&gt;Biology &amp;amp; Chemistry&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/4/what-makes-an-agent/"&gt;What makes an agent? (2025-01-04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/19/why-data-from-preclinical-biotech-lab-experiments-make-machine-learning-challenging/"&gt;Why data from preclinical biotech lab experiments make machine learning challenging (2025-01-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/23/reliable-biological-data-requires-physical-quantities-not-statistical-artifacts/"&gt;Reliable biological data requires physical quantities, not statistical artifacts (2025-02-23)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/6/a-blueprint-for-data-driven-molecule-engineering/"&gt;A blueprint for data-driven molecule engineering (2025-03-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;Bayesian Superiority Estimation with R2D2 Priors: A Practical Guide for Protein Screening (2025-04-03)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/5/from-data-chaos-to-statistical-clarity-a-laboratory-transformation-story/"&gt;From data chaos to statistical clarity: A laboratory transformation story (2025-04-05)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/19/good-practices-for-ai-assisted-development-from-a-live-protein-calculator-demo/"&gt;Good practices for AI-assisted development from a live protein calculator demo (2025-04-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/"&gt;Build your own tools! (2025-06-27)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference (2025-07-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/15/how-to-use-xarray-for-unified-laboratory-data-storage/"&gt;How to use xarray for unified laboratory data storage (2025-07-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/24/how-to-communicate-with-lab-scientists-when-youre-the-data-person/"&gt;How to communicate with lab scientists (when you're the data person) (2025-08-24)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/1/how-data-scientists-can-master-life-sciences-and-software-skills-for-biotech-using-ultralearning/"&gt;How data scientists can master life sciences and software skills for biotech using ultralearning (2025-10-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/"&gt;What does it take to build a statistics agent? (2025-12-02)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="career-advice"&gt;Career Advice&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/13/writing-at-the-speed-of-thought/"&gt;Writing at the speed of thought (2025-01-13)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/17/why-you-should-take-part-in-the-scipy-sprints/"&gt;Why you should take part in the SciPy sprints! (2025-03-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/8/why-im-excited-for-scipy-2025/"&gt;Why I'm excited for SciPy 2025! (2025-05-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference (2025-07-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/"&gt;Data scientists aren't becoming obsolete in the LLM era (2025-08-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/1/how-to-use-ai-to-accelerate-your-career-in-2025/"&gt;How to use AI to accelerate your career in 2025 (2025-09-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/1/how-data-scientists-can-master-life-sciences-and-software-skills-for-biotech-using-ultralearning/"&gt;How data scientists can master life sciences and software skills for biotech using ultralearning (2025-10-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/17/the-selfish-reason-to-do-your-best-work/"&gt;The selfish reason to do your best work (2025-12-17)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="data-science-practice-leadership"&gt;Data Science Practice &amp;amp; Leadership&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/10/a-practical-guide-to-securing-secrets-in-data-science-projects/"&gt;A practical guide to securing secrets in data science projects (2025-01-10)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/19/why-data-from-preclinical-biotech-lab-experiments-make-machine-learning-challenging/"&gt;Why data from preclinical biotech lab experiments make machine learning challenging (2025-01-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/31/pydata-bostoncambridge-talk-moderna-what-makes-an-agent/"&gt;PyData Boston/Cambridge Talk @ Moderna: What makes an agent? (2025-01-31)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/23/reliable-biological-data-requires-physical-quantities-not-statistical-artifacts/"&gt;Reliable biological data requires physical quantities, not statistical artifacts (2025-02-23)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/6/a-blueprint-for-data-driven-molecule-engineering/"&gt;A blueprint for data-driven molecule engineering (2025-03-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/16/the-art-of-finesse-as-a-data-scientist/"&gt;The art of finesse as a data scientist (2025-03-16)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/17/why-you-should-take-part-in-the-scipy-sprints/"&gt;Why you should take part in the SciPy sprints! (2025-03-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/2/how-to-standardize-data-science-ways-of-working-to-unlock-your-teams-creativity/"&gt;How to standardize Data Science ways of working to unlock your team's creativity (2025-04-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;Bayesian Superiority Estimation with R2D2 Priors: A Practical Guide for Protein Screening (2025-04-03)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/5/from-data-chaos-to-statistical-clarity-a-laboratory-transformation-story/"&gt;From data chaos to statistical clarity: A laboratory transformation story (2025-04-05)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/19/good-practices-for-ai-assisted-development-from-a-live-protein-calculator-demo/"&gt;Good practices for AI-assisted development from a live protein calculator demo (2025-04-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/8/why-im-excited-for-scipy-2025/"&gt;Why I'm excited for SciPy 2025! (2025-05-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/7/principles-for-using-ai-autodidactically/"&gt;Principles for using AI autodidactically (2025-06-07)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/"&gt;Build your own tools! (2025-06-27)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/7/the-job-your-docs-need-to-do/"&gt;The job your docs need to do (2025-07-07)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;Earn the privilege to use automation (2025-07-13)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference (2025-07-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/21/from-nerd-sniped-to-shipped-using-ai-as-a-thinking-tool/"&gt;From nerd-sniped to shipped using AI as a thinking tool (2025-07-21)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/6/stop-guessing-at-priors-r2d2s-automated-approach-to-bayesian-modeling/"&gt;Stop guessing at priors: R2D2's automated approach to Bayesian modeling (2025-08-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/"&gt;Data scientists aren't becoming obsolete in the LLM era (2025-08-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/24/how-to-communicate-with-lab-scientists-when-youre-the-data-person/"&gt;How to communicate with lab scientists (when you're the data person) (2025-08-24)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/2/the-data-science-bootstrap-notes-a-major-upgrade-for-2025/"&gt;The Data Science Bootstrap Notes: A major upgrade for 2025 (2025-09-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/1/how-data-scientists-can-master-life-sciences-and-software-skills-for-biotech-using-ultralearning/"&gt;How data scientists can master life sciences and software skills for biotech using ultralearning (2025-10-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/19/how-to-expose-any-documentation-to-any-llm-agent/"&gt;How to expose any documentation to any LLM agent (2025-10-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/"&gt;What does it take to build a statistics agent? (2025-12-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/17/the-selfish-reason-to-do-your-best-work/"&gt;The selfish reason to do your best work (2025-12-17)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="data-science-tooling"&gt;Data Science Tooling&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/4/what-makes-an-agent/"&gt;What makes an agent? (2025-01-04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/10/a-practical-guide-to-securing-secrets-in-data-science-projects/"&gt;A practical guide to securing secrets in data science projects (2025-01-10)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/13/writing-at-the-speed-of-thought/"&gt;Writing at the speed of thought (2025-01-13)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/19/why-data-from-preclinical-biotech-lab-experiments-make-machine-learning-challenging/"&gt;Why data from preclinical biotech lab experiments make machine learning challenging (2025-01-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/31/pydata-bostoncambridge-talk-moderna-what-makes-an-agent/"&gt;PyData Boston/Cambridge Talk @ Moderna: What makes an agent? (2025-01-31)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/7/lightening-the-llamabot/"&gt;Lightening the LlamaBot (2025-02-07)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/17/let-me-ship-you-the-python-you-need/"&gt;Let me ship you the Python you need (2025-02-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/23/reliable-biological-data-requires-physical-quantities-not-statistical-artifacts/"&gt;Reliable biological data requires physical quantities, not statistical artifacts (2025-02-23)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/1/how-to-fix-pypi-upload-errors-related-to-license-metadata/"&gt;How to fix PyPI upload errors related to license metadata (2025-03-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/6/a-blueprint-for-data-driven-molecule-engineering/"&gt;A blueprint for data-driven molecule engineering (2025-03-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/17/why-you-should-take-part-in-the-scipy-sprints/"&gt;Why you should take part in the SciPy sprints! (2025-03-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/2/how-to-standardize-data-science-ways-of-working-to-unlock-your-teams-creativity/"&gt;How to standardize Data Science ways of working to unlock your team's creativity (2025-04-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;Bayesian Superiority Estimation with R2D2 Priors: A Practical Guide for Protein Screening (2025-04-03)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/5/from-data-chaos-to-statistical-clarity-a-laboratory-transformation-story/"&gt;From data chaos to statistical clarity: A laboratory transformation story (2025-04-05)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/8/wow-marimo/"&gt;Wow, Marimo! (2025-04-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/19/good-practices-for-ai-assisted-development-from-a-live-protein-calculator-demo/"&gt;Good practices for AI-assisted development from a live protein calculator demo (2025-04-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/26/wow-modal/"&gt;Wow, Modal! (2025-04-26)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/8/why-im-excited-for-scipy-2025/"&gt;Why I'm excited for SciPy 2025! (2025-05-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/24/supercharge-your-coding-agents-with-vscode-workspaces/"&gt;Supercharge your coding agents with VSCode workspaces (2025-05-24)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/25/the-invisible-polish-of-automatic-model-routing/"&gt;The invisible polish of automatic model routing (2025-05-25)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/14/rethinking-llm-interfaces-from-chatbots-to-contextual-applications/"&gt;Rethinking LLM interfaces, from chatbots to contextual applications (2025-06-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/"&gt;Build your own tools! (2025-06-27)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/1/one-hour-and-eight-minutes-building-a-receipt-scanner-with-the-weirdest-tech-stack-imaginable/"&gt;One hour and eight minutes: Building a receipt scanner with the weirdest tech stack imaginable (2025-07-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;Earn the privilege to use automation (2025-07-13)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference (2025-07-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/15/how-to-use-xarray-for-unified-laboratory-data-storage/"&gt;How to use xarray for unified laboratory data storage (2025-07-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/21/from-nerd-sniped-to-shipped-using-ai-as-a-thinking-tool/"&gt;From nerd-sniped to shipped using AI as a thinking tool (2025-07-21)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/6/stop-guessing-at-priors-r2d2s-automated-approach-to-bayesian-modeling/"&gt;Stop guessing at priors: R2D2's automated approach to Bayesian modeling (2025-08-06)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/"&gt;Data scientists aren't becoming obsolete in the LLM era (2025-08-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/23/wicked-python-trickery-dynamically-patch-a-python-functions-source-code-at-runtime/"&gt;Wicked Python trickery - dynamically patch a Python function's source code at runtime (2025-08-23)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/2/the-data-science-bootstrap-notes-a-major-upgrade-for-2025/"&gt;The Data Science Bootstrap Notes: A major upgrade for 2025 (2025-09-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/"&gt;How to teach your coding agent with AGENTS.md (2025-10-04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/10/how-to-use-multiple-github-accounts-on-the-same-computer/"&gt;How to use multiple GitHub accounts on the same computer (2025-10-10)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/14/how-to-use-coding-agents-effectively/"&gt;How to Use Coding Agents Effectively (2025-10-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/18/a-practical-comparison-of-dspy-and-llamabot-for-structured-llm-applications/"&gt;A practical comparison of DSPy and LlamaBot for structured LLM applications (2025-10-18)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/19/how-to-expose-any-documentation-to-any-llm-agent/"&gt;How to expose any documentation to any LLM agent (2025-10-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/20/exploring-skills-vs-mcp-servers/"&gt;Exploring Skills vs MCP Servers (2025-10-20)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/28/use-coding-agents-to-write-marimo-notebooks/"&gt;Use coding agents to write Marimo notebooks (2025-10-28)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/8/safe-ways-to-let-your-coding-agent-work-autonomously/"&gt;Safe ways to let your coding agent work autonomously (2025-11-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;How I Replaced 307 Lines of Agent Code with 4 Lines (2025-11-16)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/17/how-to-reference-code-across-repositories-with-coding-agents/"&gt;How to Reference Code Across Repositories with Coding Agents (2025-11-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/"&gt;What does it take to build a statistics agent? (2025-12-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/10/productive-patterns-for-agent-assisted-programming/"&gt;Productive Patterns for Agent-Assisted Programming (2025-12-10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="llms"&gt;LLMs&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/4/what-makes-an-agent/"&gt;What makes an agent? (2025-01-04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/31/pydata-bostoncambridge-talk-moderna-what-makes-an-agent/"&gt;PyData Boston/Cambridge Talk @ Moderna: What makes an agent? (2025-01-31)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/7/lightening-the-llamabot/"&gt;Lightening the LlamaBot (2025-02-07)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/8/wow-marimo/"&gt;Wow, Marimo! (2025-04-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/26/wow-modal/"&gt;Wow, Modal! (2025-04-26)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/8/why-im-excited-for-scipy-2025/"&gt;Why I'm excited for SciPy 2025! (2025-05-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/24/supercharge-your-coding-agents-with-vscode-workspaces/"&gt;Supercharge your coding agents with VSCode workspaces (2025-05-24)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/25/the-invisible-polish-of-automatic-model-routing/"&gt;The invisible polish of automatic model routing (2025-05-25)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/7/principles-for-using-ai-autodidactically/"&gt;Principles for using AI autodidactically (2025-06-07)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/14/rethinking-llm-interfaces-from-chatbots-to-contextual-applications/"&gt;Rethinking LLM interfaces, from chatbots to contextual applications (2025-06-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/"&gt;Build your own tools! (2025-06-27)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/1/one-hour-and-eight-minutes-building-a-receipt-scanner-with-the-weirdest-tech-stack-imaginable/"&gt;One hour and eight minutes: Building a receipt scanner with the weirdest tech stack imaginable (2025-07-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;Earn the privilege to use automation (2025-07-13)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference (2025-07-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/21/from-nerd-sniped-to-shipped-using-ai-as-a-thinking-tool/"&gt;From nerd-sniped to shipped using AI as a thinking tool (2025-07-21)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/"&gt;Data scientists aren't becoming obsolete in the LLM era (2025-08-15)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/23/wicked-python-trickery-dynamically-patch-a-python-functions-source-code-at-runtime/"&gt;Wicked Python trickery - dynamically patch a Python function's source code at runtime (2025-08-23)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/1/how-to-use-ai-to-accelerate-your-career-in-2025/"&gt;How to use AI to accelerate your career in 2025 (2025-09-01)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/"&gt;How to teach your coding agent with AGENTS.md (2025-10-04)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/14/how-to-use-coding-agents-effectively/"&gt;How to Use Coding Agents Effectively (2025-10-14)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/18/a-practical-comparison-of-dspy-and-llamabot-for-structured-llm-applications/"&gt;A practical comparison of DSPy and LlamaBot for structured LLM applications (2025-10-18)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/19/how-to-expose-any-documentation-to-any-llm-agent/"&gt;How to expose any documentation to any LLM agent (2025-10-19)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/20/exploring-skills-vs-mcp-servers/"&gt;Exploring Skills vs MCP Servers (2025-10-20)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/28/use-coding-agents-to-write-marimo-notebooks/"&gt;Use coding agents to write Marimo notebooks (2025-10-28)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/8/safe-ways-to-let-your-coding-agent-work-autonomously/"&gt;Safe ways to let your coding agent work autonomously (2025-11-08)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;How I Replaced 307 Lines of Agent Code with 4 Lines (2025-11-16)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/17/how-to-reference-code-across-repositories-with-coding-agents/"&gt;How to Reference Code Across Repositories with Coding Agents (2025-11-17)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/"&gt;What does it take to build a statistics agent? (2025-12-02)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/10/productive-patterns-for-agent-assisted-programming/"&gt;Productive Patterns for Agent-Assisted Programming (2025-12-10)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="all-blog-posts"&gt;All blog posts&lt;/h2&gt;&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Date&lt;/th&gt;
&lt;th&gt;Title&lt;/th&gt;
&lt;th&gt;Categories&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2025-01-04&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/4/what-makes-an-agent/"&gt;What makes an agent?&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-01-10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/10/a-practical-guide-to-securing-secrets-in-data-science-projects/"&gt;A practical guide to securing secrets in data science projects&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-01-13&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/13/writing-at-the-speed-of-thought/"&gt;Writing at the speed of thought&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-01-19&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/19/why-data-from-preclinical-biotech-lab-experiments-make-machine-learning-challenging/"&gt;Why data from preclinical biotech lab experiments make machine learning challenging&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-01-31&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/1/31/pydata-bostoncambridge-talk-moderna-what-makes-an-agent/"&gt;PyData Boston/Cambridge Talk @ Moderna: What makes an agent?&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling, Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-02-07&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/7/lightening-the-llamabot/"&gt;Lightening the LlamaBot&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-02-17&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/17/let-me-ship-you-the-python-you-need/"&gt;Let me ship you the Python you need&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-02-23&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/2/23/reliable-biological-data-requires-physical-quantities-not-statistical-artifacts/"&gt;Reliable biological data requires physical quantities, not statistical artifacts&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Biology &amp;amp; Chemistry, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-01&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/1/how-to-fix-pypi-upload-errors-related-to-license-metadata/"&gt;How to fix PyPI upload errors related to license metadata&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-06&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/6/a-blueprint-for-data-driven-molecule-engineering/"&gt;A blueprint for data-driven molecule engineering&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-16&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/16/the-art-of-finesse-as-a-data-scientist/"&gt;The art of finesse as a data scientist&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-03-17&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/3/17/why-you-should-take-part-in-the-scipy-sprints/"&gt;Why you should take part in the SciPy sprints!&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-02&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/2/how-to-standardize-data-science-ways-of-working-to-unlock-your-teams-creativity/"&gt;How to standardize Data Science ways of working to unlock your team's creativity&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-03&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;Bayesian Superiority Estimation with R2D2 Priors: A Practical Guide for Protein Screening&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-05&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/5/from-data-chaos-to-statistical-clarity-a-laboratory-transformation-story/"&gt;From data chaos to statistical clarity: A laboratory transformation story&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-08&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/8/wow-marimo/"&gt;Wow, Marimo!&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling, LLMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-19&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/19/good-practices-for-ai-assisted-development-from-a-live-protein-calculator-demo/"&gt;Good practices for AI-assisted development from a live protein calculator demo&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-04-26&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/4/26/wow-modal/"&gt;Wow, Modal!&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling, LLMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-05-08&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/8/why-im-excited-for-scipy-2025/"&gt;Why I'm excited for SciPy 2025!&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-05-24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/24/supercharge-your-coding-agents-with-vscode-workspaces/"&gt;Supercharge your coding agents with VSCode workspaces&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-05-25&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/5/25/the-invisible-polish-of-automatic-model-routing/"&gt;The invisible polish of automatic model routing&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-06-07&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/7/principles-for-using-ai-autodidactically/"&gt;Principles for using AI autodidactically&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-06-14&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/14/rethinking-llm-interfaces-from-chatbots-to-contextual-applications/"&gt;Rethinking LLM interfaces, from chatbots to contextual applications&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-06-27&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/"&gt;Build your own tools!&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-01&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/1/one-hour-and-eight-minutes-building-a-receipt-scanner-with-the-weirdest-tech-stack-imaginable/"&gt;One hour and eight minutes: Building a receipt scanner with the weirdest tech stack imaginable&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-07&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/7/the-job-your-docs-need-to-do/"&gt;The job your docs need to do&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-13&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;Earn the privilege to use automation&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-14&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/"&gt;Reflections on the SciPy 2025 Conference&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling, Biology &amp;amp; Chemistry, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-15&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/15/how-to-use-xarray-for-unified-laboratory-data-storage/"&gt;How to use xarray for unified laboratory data storage&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-07-21&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/7/21/from-nerd-sniped-to-shipped-using-ai-as-a-thinking-tool/"&gt;From nerd-sniped to shipped using AI as a thinking tool&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-08-06&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/6/stop-guessing-at-priors-r2d2s-automated-approach-to-bayesian-modeling/"&gt;Stop guessing at priors: R2D2's automated approach to Bayesian modeling&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-08-15&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/"&gt;Data scientists aren't becoming obsolete in the LLM era&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Practice &amp;amp; Leadership, Data Science Tooling, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-08-23&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/23/wicked-python-trickery-dynamically-patch-a-python-functions-source-code-at-runtime/"&gt;Wicked Python trickery - dynamically patch a Python function's source code at runtime&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-08-24&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/8/24/how-to-communicate-with-lab-scientists-when-youre-the-data-person/"&gt;How to communicate with lab scientists (when you're the data person)&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Biology &amp;amp; Chemistry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-09-01&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/1/how-to-use-ai-to-accelerate-your-career-in-2025/"&gt;How to use AI to accelerate your career in 2025&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-09-02&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/9/2/the-data-science-bootstrap-notes-a-major-upgrade-for-2025/"&gt;The Data Science Bootstrap Notes: A major upgrade for 2025&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-01&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/1/how-data-scientists-can-master-life-sciences-and-software-skills-for-biotech-using-ultralearning/"&gt;How data scientists can master life sciences and software skills for biotech using ultralearning&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Practice &amp;amp; Leadership, Biology &amp;amp; Chemistry, Career Advice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-04&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/"&gt;How to teach your coding agent with AGENTS.md&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/10/how-to-use-multiple-github-accounts-on-the-same-computer/"&gt;How to use multiple GitHub accounts on the same computer&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-14&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/14/how-to-use-coding-agents-effectively/"&gt;How to Use Coding Agents Effectively&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-18&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/18/a-practical-comparison-of-dspy-and-llamabot-for-structured-llm-applications/"&gt;A practical comparison of DSPy and LlamaBot for structured LLM applications&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-19&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/19/how-to-expose-any-documentation-to-any-llm-agent/"&gt;How to expose any documentation to any LLM agent&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling, Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-20&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/20/exploring-skills-vs-mcp-servers/"&gt;Exploring Skills vs MCP Servers&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-10-28&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/10/28/use-coding-agents-to-write-marimo-notebooks/"&gt;Use coding agents to write Marimo notebooks&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-11-08&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/8/safe-ways-to-let-your-coding-agent-work-autonomously/"&gt;Safe ways to let your coding agent work autonomously&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-11-16&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;How I Replaced 307 Lines of Agent Code with 4 Lines&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-11-17&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/11/17/how-to-reference-code-across-repositories-with-coding-agents/"&gt;How to Reference Code Across Repositories with Coding Agents&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Data Science Tooling, LLMs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-12-02&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/"&gt;What does it take to build a statistics agent?&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling, Biology &amp;amp; Chemistry, Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-12-10&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/10/productive-patterns-for-agent-assisted-programming/"&gt;Productive Patterns for Agent-Assisted Programming&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;LLMs, Data Science Tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2025-12-17&lt;/td&gt;
&lt;td&gt;&lt;a href="https://ericmjl.github.io/blog/2025/12/17/the-selfish-reason-to-do-your-best-work/"&gt;The selfish reason to do your best work&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Career Advice, Data Science Practice &amp;amp; Leadership&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
</content></entry><entry><title>The selfish reason to do your best work</title><link href="https://ericmjl.github.io/blog/2025/12/17/the-selfish-reason-to-do-your-best-work/" rel="alternate"/><updated>2025-12-17T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0e85eb9c-a072-3e9f-8de1-dbcb432865e4</id><content type="html">&lt;p&gt;I’ve been thinking a lot about career lately. This has been a pretty lean year for biotech; we've seen ups and downs at Moderna and across the industry. So, I want to offer a word of encouragement and a philosophy on work that I hope can be useful for you, regardless of where you are in your journey.&lt;/p&gt;
&lt;p&gt;It starts with a reframing of &lt;em&gt;why&lt;/em&gt; we work.&lt;/p&gt;
&lt;h3 id="do-your-best-work-for-yourself"&gt;Do your best work for yourself&lt;/h3&gt;&lt;p&gt;I know there is a lot of sentiment going around the internet right now about "acting your wage"—limiting your effort to exactly what you are paid for—or doing the bare minimum. The logic goes: &lt;em&gt;Why should I care about doing my best for my job if my company doesn't care for me?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I get where that sentiment comes from. But I want to redirect your attention a little bit.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You do not have to do your best work for your company. You should do your best work for &lt;em&gt;yourself&lt;/em&gt; at the company you’re at.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;It just so happens that the company will benefit, but you should treat your effort as an investment in your own professional instincts and habits. Sooner or later, you may not be at that company. You actually don't really have to care about the entity itself. If you don't care about the people who work at your workplace, no one is compelling you to. (Though, if you do happen to like your colleagues—which is true for me where I work—then that’s all good.)&lt;/p&gt;
&lt;p&gt;But even if you can’t find much to be inspired by, do your best work anyway. Why? Because you are building the person you will be in five or ten years.&lt;/p&gt;
&lt;p&gt;President Obama once gave this advice to young interns: "Don't ask for the plum assignments. Just knock out everything you're doing." I guarantee you someone will notice. Even if no one at your current company notices, if you build a track record of quality, people &lt;em&gt;outside&lt;/em&gt; will notice.&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/YNY4UFaHbP4?si=_tkyjLX25IWCfF0L" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;p&gt;Your reputation precedes you. It is the one thing you accumulate over time that serves as a form of wealth that can never be taken away from you. Only you can lose it. To borrow a phrase from Jocko Willink, this is "Extreme Ownership"—taking total responsibility for your world. Yes, circumstances happen, but if you guard your reputation well, it is yours to keep.&lt;/p&gt;
&lt;p&gt;Think about it: Who knows where you will be ten years down the road? If you are a software engineer or data scientist now, in five years you might be a Director. You’re going to be calling the shots. If you don't take the time &lt;em&gt;now&lt;/em&gt; to practice making decisions, witnessing judgment calls, and battle-testing your engineering foresight, will you be ready?&lt;/p&gt;
&lt;p&gt;I had a former teammate who worked under me at Moderna, &lt;a href="https://www.linkedin.com/in/arkadij-kummer-78b249b9"&gt;Arkadij Kummer&lt;/a&gt;. He’s now the CTO of a startup—a title I haven't even held. I saw him put in the effort to develop the strategic thinking patterns that helped him get the skills he needed to lead a tech organization. He sought out opportunities to practice making judgment calls and owning the outcomes. You have to get those reps in early, or you won’t be ready for what happens later.&lt;/p&gt;
&lt;p&gt;So, if you are working at a company you want to leave: do not give up on investing in yourself. Fortune favors the prepared.&lt;/p&gt;
&lt;h3 id="resilience-is-also-an-investment"&gt;Resilience is also an investment&lt;/h3&gt;&lt;p&gt;Building this "career wealth" isn't just about technical execution. It's also about how you handle failure. And here is the second word of encouragement I want to offer: &lt;strong&gt;Everyone will make a blunder at some point.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Part of growing up—and part of investing in your own character—is owning up to those mistakes, being proactive about remedying them, and graciously accepting help.&lt;/p&gt;
&lt;p&gt;I have been there. I once wrote a very scathing internal blog post about my leadership at a previous company. Looking back, I sometimes think of it as the darkest weekend of my career.&lt;/p&gt;
&lt;p&gt;I was a guy who had just finished school, entered the workforce, and within three years decided I knew better than people who had done their jobs for twenty-odd years. I'm not saying it's impossible that I was right, but it was pretty presumptuous. I lashed out at other groups that I thought were incompetent, effectively attacking my own team.&lt;/p&gt;
&lt;p&gt;The leadership team responded with extreme grace. They knew the problems I outlined were real, but they also saw a junior person who hadn't picked his battles wisely.&lt;/p&gt;
&lt;p&gt;We have limited energy and limited ability to focus. There are only so many battles we can handle simultaneously. I chose a bad battle to fight.&lt;/p&gt;
&lt;p&gt;That experience prompted deep reflection. I decided to lean back into that first philosophy: I was going to go back and do a good job. Not necessarily because I was feeling proud of the company at that moment, but because I recognized that if I ever wanted to be the type of person with the authority to change things, I needed to be the best version of myself first. I needed to learn how to handle authority and how to elevate the people around me.&lt;/p&gt;
&lt;p&gt;If you are in a situation where you’ve made a mistake, the best thing you can do for your reputation is to own it. Propose an action to rectify it. Move on.&lt;/p&gt;
&lt;p&gt;Most mistakes are not unforgivable. There is a classic business story about Tom Watson, the founder of IBM, involving a subordinate who made a mistake that cost the company \$600,000. Whether the amount was really \$600,000 or not, the story has a lesson that rings true. The man walked into Watson's office expecting to be fired. Watson reportedly replied, "No, I just spent \$600,000 training you. Why would I want to fire you?"&lt;/p&gt;
&lt;p&gt;Most people will understand. Yes, there will be delays. That is just the world we inhabit. Own the mistake, improve the process, and keep making it better. Again, not primarily for the company, but because you are preparing yourself to lead with integrity and compassion.&lt;/p&gt;
&lt;h3 id="the-wealth-that-remains"&gt;The wealth that remains&lt;/h3&gt;&lt;p&gt;I don't want you to become the whiner or the complainer. That is a habit that will stick with you for the rest of your life.&lt;/p&gt;
&lt;p&gt;Instead, I want you to play the long game. Don't let temporary frustrations dictate your long-term growth. Anything worth doing is going to be difficult. If it was easy, the reward would be fleeting. Even for the ultra-rich, like Sergey Brin, eventually the yacht gets boring. He returned to active coding at Google to work on AI because, fundamentally, humans are wired to find satisfaction in building something meaningful.&lt;/p&gt;
&lt;p&gt;So, aspire to greatness. Not just to gain a title or a promotion, but because in the process, you accumulate a wealth that cannot be taken away from you: You will have the satisfaction of mastery. You will have a battle-tested character. You will have a reputation that opens doors before you even knock.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do your best work.&lt;/strong&gt; It’s the best investment you’ll ever make. And it'll be the most selfless gift you give to yourself and others.&lt;/p&gt;
</content></entry><entry><title>Productive Patterns for Agent-Assisted Programming</title><link href="https://ericmjl.github.io/blog/2025/12/10/productive-patterns-for-agent-assisted-programming/" rel="alternate"/><updated>2025-12-10T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:fe2a71e9-46d4-3221-a3fa-3f7dd32c2786</id><content type="html">&lt;p&gt;I've been using coding agents for a while now, and I've learned a few patterns that make the experience much more productive. The thing is, a lot of these "productive patterns" aren't being shared enough—they're more like folk knowledge that you can only really pick up by watching someone else do their work live. I decided to write this blog post to kickstart conversations about the matter. Here's what works for me.&lt;/p&gt;
&lt;h2 id="build-a-detailed-plan-with-ai"&gt;Build a detailed plan with AI&lt;/h2&gt;&lt;p&gt;Before jumping into implementation, spend time building a detailed plan with your AI assistant. Iterate 2-3 times over the plan, checking every detail. You want the ability to see in your head what the code might look like—just a "fat finger sketch" of the implementation.&lt;/p&gt;
&lt;p&gt;The plan should include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Details on implementation&lt;/li&gt;
&lt;li&gt;How to test (this is the most important part)&lt;/li&gt;
&lt;li&gt;Documentation plan&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="do-docs-and-tests-first"&gt;Do docs and tests first&lt;/h2&gt;&lt;p&gt;Humans usually adhere to test-driven development if you're a software engineer, or exploration-driven software builds if you're more of a data scientist. Because of the sequential nature of generative AI, it's advantageous to instruct AI to do the docs and tests first before the implementation. This is a complex conditional probability problem. If the tests and docs are written first, the implementation has to satisfy those constraints, which leads to better code.&lt;/p&gt;
&lt;p&gt;The test plan should include instructions on how to run tests using command line tools. Don't assume the AI knows your project's specific testing setup.&lt;/p&gt;
&lt;h2 id="use-agents-md-as-your-repo-s-ai-university"&gt;Use AGENTS.md as your repo's AI university&lt;/h2&gt;&lt;p&gt;AGENTS.md is a great place to store the specific instructions that you need for the repo. For example, AI will tend to write &lt;code&gt;python -m ...&lt;/code&gt; as a shell command, but if I'm running a pixi project, it's better to always run &lt;code&gt;pixi run python ...&lt;/code&gt; instead. Treat AGENTS.md as the AI's university of your particular repo; it's where you encode all the project-specific knowledge that the AI needs to work effectively.&lt;/p&gt;
&lt;h2 id="control-the-pace-of-the-agent"&gt;Control the pace of the agent&lt;/h2&gt;&lt;p&gt;Know the default behavior of your agent; it may be over-eager to do lots of things. You can pace the coding agent by asking it to "slow down, walk me through the changes one at a time, starting with the most important ones first." This helps you maintain control and review changes as they happen, rather than being overwhelmed by a massive diff.&lt;/p&gt;
&lt;h2 id="leverage-local-and-command-line-tools"&gt;Leverage local and command line tools&lt;/h2&gt;&lt;p&gt;You can use local and command line tools to your advantage! Here are some examples:&lt;/p&gt;
&lt;p&gt;Firstly, the GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) can be used to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Store plans on GitHub as issues first (a matter of taste—you can avoid cluttering up your local filesystem)&lt;/li&gt;
&lt;li&gt;Pull GitHub Actions logs&lt;/li&gt;
&lt;li&gt;Call out to the GitHub API for other general tasks&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Environment management:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pixi run&lt;/code&gt; ensures you're always running within the correct Python environment&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx marimo check&lt;/code&gt; lets me check that marimo notebooks are syntactically valid&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uv run notebook.py&lt;/code&gt; lets me run notebooks as scripts to check outputs&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uvx marimo export&lt;/code&gt; lets me export marimo notebooks as markdown&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Linting and quality:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;markdownlint&lt;/code&gt; runs on every edit of markdown files so you never have markdown linting issues&lt;/li&gt;
&lt;li&gt;Get AI to "commit relevant files and fix issues raised by pre-commit hooks"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Let agents use CLI tools and read outputs directly so that you don't have to switch between windows copying and pasting things manually.&lt;/p&gt;
&lt;h2 id="let-agents-write-temporary-tools"&gt;Let agents write temporary tools&lt;/h2&gt;&lt;p&gt;Coding agents can write their own temporary tools inside &lt;code&gt;.py&lt;/code&gt; files. Encourage coding agents to do that to test that what it wrote works on-the-fly. This is a great way to validate code before integrating it into your main codebase.&lt;/p&gt;
&lt;p&gt;You can even experiment with self-improving agents: if it detects you correcting its action, it should auto-update AGENTS.md with what is the correct thing to do. I haven't fully battle-tested this yet, but you can write an "AI constitution" at the top of AGENTS.md that instructs the agent to learn from corrections by &lt;em&gt;remembering them inside AGENTS.md&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="develop-your-own-tools"&gt;Develop your own tools&lt;/h2&gt;&lt;p&gt;Isabel Zimmerman mentioned this in her keynote talk: develop your own tools. Here are some examples of my own:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Personal MCP productivity server&lt;/strong&gt;: gives me prompts that I can take from project to project, so I don't have to keep copying/pasting them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shell aliases&lt;/strong&gt;: &lt;code&gt;gacp&lt;/code&gt; lets me run &lt;code&gt;git add . &amp;amp;&amp;amp; git commit &amp;amp;&amp;amp; git push&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LlamaBot git hooks&lt;/strong&gt;: auto-writes commit messages for me&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These custom tools compound over time and make your workflow significantly more efficient.&lt;/p&gt;
&lt;p&gt;These patterns have made my agent-assisted programming much more productive. Treat the AI as a collaborator that needs clear instructions, proper context, and the right tools to work effectively. Start with a good plan, control the pace, and build tools that make the whole process smoother.&lt;/p&gt;
&lt;p&gt;What patterns have you discovered? I'd love to hear what works for you—let's make this folk knowledge more accessible to everyone.&lt;/p&gt;
</content></entry><entry><title>What does it take to build a statistics agent?</title><link href="https://ericmjl.github.io/blog/2025/12/2/what-does-it-take-to-build-a-statistics-agent/" rel="alternate"/><updated>2025-12-02T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:3edf8f61-a0a7-3f5c-b699-ee70ca69638e</id><content type="html">&lt;p&gt;Within research organizations at most pharma and biotech companies, professionally-trained statisticians are often staffed at extremely low ratios relative to the number of lab scientists. By rough Fermi estimation, I'd hazard a guess that ratios anywhere from 1:10 to 1:100 are plausible, meaning most researchers have limited access to statistical expertise when they need it most, during experiment design. This statistician shortage creates a critical bottleneck in experimental design, power calculations, and biostatistical consultation—areas where proper statistical guidance can prevent costly mistakes and improve research reproducibility.&lt;/p&gt;
&lt;p&gt;This creates a costly problem. When statisticians aren't available, researchers fall back to what I call "folk statistics" - the kind you learn by immersion in a lab, or from 1-2 graduate lectures hidden within broader "laboratory methods" or "computational methods" classes. I know this because I practiced folk statistics myself in the life sciences, blindly following rules like "just do n=3" or "just use the t-test with your count data" without understanding the statistical reasoning behind these choices.&lt;/p&gt;
&lt;p&gt;The consequences are documented in stark numbers. Amgen scientists attempted to reproduce 53 landmark preclinical papers and failed in 47 cases (89%)—even after contacting original authors and exchanging reagents. Bayer's internal validation found only 20-25% of studies "completely in line" with original publications. These studies consistently identified poor experimental design and inadequate statistical analysis as major contributors. &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC4461318/"&gt;Freedman et al. (2015)&lt;/a&gt; estimated $28 billion annually spent on irreproducible preclinical research in the United States alone.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://cdn.ncbi.nlm.nih.gov/pmc/blobs/ccea/4461318/0e3c7aea2b45/pbio.1002165.g002.jpg" alt="Breakdown of causes of preclinical irreproducibility from Freedman et al. (2015). Study design accounts for 27.6% of irreproducibility."&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Breakdown of causes of preclinical irreproducibility from Freedman et al. (2015). Study design accounts for 27.6% of irreproducibility.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;At the individual experiment level, this translates to teams &lt;strong&gt;throwing out hard-won experimental data&lt;/strong&gt; that can cost anywhere from thousands to hundreds of thousands of dollars to collect per round, &lt;strong&gt;wasting up to millions of dollars downstream&lt;/strong&gt; by basing decisions on poorly-collected data, and &lt;strong&gt;missing opportunities&lt;/strong&gt; to set up machine learners with high quality laboratory data that could shortcut the amount of laboratory experimentation needed.&lt;/p&gt;
&lt;p&gt;I took one semester of graduate-level biostatistics, then a decade of self-study in Bayesian statistics, followed by professional work where accurate estimation was critical—whether estimating half-life of a molecule, binding affinity of an antibody, or other performance properties. Through this journey, I no longer trust folk statistics. Folk statistics relies on faulty assumptions—like "n=3 is all you'll really need," "use the t-test for count data," or "calculate the SEM and don't show the SD"—which influence bad decision-making when people don't know better. Once you see how these assumptions break down and lead to wrong conclusions, you can't unsee it. Quantities like half-life, binding affinity, and other performance properties need to be accurately estimated through proper experimental design and statistically-informed mechanistic modeling.&lt;/p&gt;
&lt;p&gt;Statisticians are expensive, but they're also 100% critical for generating high quality, high fidelity data. Their role at the experiment design phase is usually that of a consultant, asking probing questions to ensure experiments are designed with good controls, confounders are accounted for, and the right statistical models are chosen. The question is: &lt;strong&gt;can we scale this expertise?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Not to replace statisticians, but to level up the organizational statistical practice &lt;em&gt;before&lt;/em&gt; researchers check in with a professionally-trained stats person. If lab scientists can think through their experimental designs more rigorously beforehand - understanding power calculations, considering confounders, planning proper controls - then the conversations they have with statisticians can be elevated. Instead of starting from scratch, they can engage in more sophisticated discussions about design trade-offs, model selection, and advanced statistical considerations. In turn, this amplifies the value of the statistician's time and improves outcomes for everyone.&lt;/p&gt;
&lt;p&gt;I was inspired by Dr. Emi Tanaka's &lt;a href="https://emitanaka.org/slides/AASC2024/#/title-slide"&gt;slides on extracting elements of statistical experiment design using LLMs&lt;/a&gt;, which showed how we can extract structured information like response variables, treatments, experimental units, design types, replicate structure, and controls. I decided to take a stab at building something that could do more than just extract information—something that could actually consult on experiment design.&lt;/p&gt;
&lt;p&gt;And so &lt;code&gt;stats-agents&lt;/code&gt; was born: an AI-powered statistics agent for experiment design consultation. Here's how I designed and evaluated this domain-specific AI agent.&lt;/p&gt;
&lt;p&gt;As a preface, I initially explored the ReAct pattern but &lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;switched to PocketFlow&lt;/a&gt;, a minimalist graph-based framework that replaced 307 lines of agent orchestration code with just 4 lines. This graph-based approach brought clarity, modularity, and made the execution flow explicit—exactly what I needed for building a robust statistics agent.&lt;/p&gt;
&lt;p&gt;So how did I go about building this agent?&lt;/p&gt;
&lt;p&gt;Deeply influenced by Clayton Christensen's books, I actually started with "what's the job for this agent to be done?" I initially considered building a single agent that could handle both experiment design consultation and statistical analysis of collected data. However, I quickly realized these are fundamentally different phases with different goals, tools, and interaction patterns.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;experiment design phase&lt;/strong&gt; is consultative and exploratory - it's about asking questions, understanding constraints, identifying potential issues, and helping researchers think through their design &lt;em&gt;before&lt;/em&gt; data collection. The &lt;strong&gt;analysis phase&lt;/strong&gt; is more technical - it's about taking collected data and building statistical models to estimate quantities of interest.&lt;/p&gt;
&lt;p&gt;I decided to focus the agent on the design phase only. This separation of concerns made the agent cleaner, less confusing, and allowed it to be optimized for its specific purpose: being an inquisitive, consultative partner during experiment design. The analysis phase would be handled separately (or by a different agent) with its own tools and prompting strategies.&lt;/p&gt;
&lt;p&gt;So I defined the agent's job description (JD) as: "an agent that will provide critique on experiment designs, suggest modifications, and help researchers think through their experimental design before data collection". It sounded oddly like a human's job description for a real job, except more specific. Notice, however, that the JD leaves room for a real human, in that no accountability for outcomes is placed on the agent, a human statistician still needs to review the work, just as we wrote above.&lt;/p&gt;
&lt;p&gt;With the job scope defined, I turned to designing the tools the agent would need.&lt;/p&gt;
&lt;p&gt;The first tool I gave was &lt;code&gt;critique_experiment_design&lt;/code&gt; - a tool that provides comprehensive critique of experiment designs, identifying potential flaws, biases, weaknesses, and areas for improvement. This tool considers multiple angles including biological, statistical, and practical constraints. The agent can use this to help researchers identify issues in their designs before they collect data.&lt;/p&gt;
&lt;p&gt;The second tool I gave was one I previously wrote about: the ability to execute code (&lt;code&gt;write_and_execute_code&lt;/code&gt;). I wanted this available to answer questions like "what should my data table look like?"&lt;/p&gt;
&lt;p&gt;I've noticed that having a sample data table in front of us when discussing experimental designs is incredibly clarifying—it cuts through abstract confusion toward concrete understanding. This tool enables the agent to generate sample data tables, perform power calculations, create plate map visualizations, and other dynamic analyses.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important security note&lt;/strong&gt;: The ability to execute arbitrary Python code is powerful but also dangerous. An agent that can execute code can delete files, modify system configurations, access sensitive data, make network requests, and more. For any production deployment, this agent must run in a containerized environment with strict isolation, resource limits, and no access to secrets or credentials. This isn't optional - it's a cybersecurity requirement! For my development and testing, I ran it in a controlled environment on my own machine, but production deployment would require proper containerization.&lt;/p&gt;
&lt;p&gt;With the tools defined, the next challenge was evaluation: how do you know if the agent is actually working? This turned out to be more complex than I initially expected.&lt;/p&gt;
&lt;p&gt;The evaluation process had two distinct phases: an exploration-guided MVP phase (vibes-driven) and a post-MVP systematic testing phase.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 1: MVP Development (Vibes-Driven)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;During the MVP phase, I defined a fixed conversation path and repeatedly tested it manually in a Marimo notebook's chat UI. I tested with a sequence like: asking for design critique, providing an experiment description, requesting power calculations, and asking for sample data tables. As I found errors, I fixed them immediately, contrary to evaluation best practices, but appropriate for this exploratory phase. The tool definitions weren't settled until this phase was complete.&lt;/p&gt;
&lt;p&gt;I also used Cursor (a coding agent) to help diagnose issues, explore multiple solutions, and get different perspectives before committing to fixes. This "multiple AI opinions before committing" pattern follows a similar philosophy to Geoffrey Litt's &lt;a href="https://www.geoffreylitt.com/2025/10/24/code-like-a-surgeon"&gt;"Code like a surgeon"&lt;/a&gt; approach: spike out an attempt at a big change, review it as a sketch of where to go, and often you won't use the result directly—but it helps you understand the problem space better.&lt;/p&gt;
&lt;p&gt;Rather than accepting the first AI suggestion, I'd ask multiple questions, explore several solution approaches, and understand the trade-offs before making an informed decision. When debugging complex issues like the closure vs. shared state problem, I'd often ask multiple models the same question to see if they'd converge on the same diagnosis—if different LLMs independently arrived at the same answer, that was a good sign the solution was on the right track. This led to better architecture decisions and fewer instances of "I wish I had done it differently."&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Phase 2: Systematic Evaluation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Once the baseline behavior was satisfactory, I moved to systematic evaluation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Benchmark prompt&lt;/strong&gt;: I created a "perfect prompt" document (&lt;code&gt;experiment_design_for_power_calc.md&lt;/code&gt;) that served as my regression test suite. This complete, detailed experiment design specification should trigger specific agent behaviors, such as asking the right questions, performing power calculations correctly, providing contextual explanations. Every time I modified the system prompt or tools, I'd run this same benchmark and check: did it still work? Without this, I found myself making changes that broke things in subtle ways, or losing track of what "good" behavior even looked like. &lt;em&gt;The benchmark prompt became my north star, a concrete example of the agent working as intended.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Synthetic test generation&lt;/strong&gt;: Once I had a reliable benchmark, I expanded to systematic evaluation with variation. Starting with five examples from Tanaka's slide deck, I used the methodology from Shreya Shankar and Hamel Husain (researchers who developed systematic approaches for LLM evaluation) to generate synthetic chat examples by selecting 1-3 axes of variation. I chose experimental domain (biotech vs. agriculture) as the primary axis, while varying the statistical expertise level of the simulated user. This process generated dozens of conversation traces that, while not exhaustive, represented draws from my constrained prior belief about likely conversations.&lt;/p&gt;
&lt;p&gt;Systematic evaluation with these varied conversation traces revealed patterns I never would have noticed through manual testing. Through these traces, I identified three major categories of failure modes that needed to be addressed.&lt;/p&gt;
&lt;h2 id="failure-mode-1-multi-step-execution-breakdown"&gt;Failure mode 1: Multi-step execution breakdown&lt;/h2&gt;&lt;p&gt;The first major issue I encountered was that the agent couldn't chain tool calls effectively. When the agent executed code to perform a power calculation, it would store the result in a variable like &lt;code&gt;mtt_power_analysis_result&lt;/code&gt;. But when it tried to analyze that result in a subsequent tool call, it would fail with a &lt;code&gt;NameError&lt;/code&gt; - the variable simply wasn't accessible.&lt;/p&gt;
&lt;p&gt;The root cause was subtle: the code execution tool (&lt;code&gt;write_and_execute_code_wrapper&lt;/code&gt;) was using a closure variable that captured the notebook's globals at initialization time. However, the agent framework (AgentBot) stores results in a separate shared dictionary (&lt;code&gt;shared["globals_dict"]&lt;/code&gt;). These two dictionaries were disconnected; think of them as two separate notebooks that couldn't see each other's variables. So when the agent created a variable in one tool call, it wasn't visible to the next.&lt;/p&gt;
&lt;p&gt;The fix required connecting them: I modified the code execution tool to accept an optional &lt;code&gt;_globals_dict&lt;/code&gt; parameter. When provided, it uses the agent's shared dictionary instead of its own isolated one. This allows results from one tool call to be accessible in subsequent calls, enabling true multi-step workflows where the agent can build on previous results.&lt;/p&gt;
&lt;h2 id="failure-mode-2-display-formatting-and-contextual-output"&gt;Failure mode 2: Display formatting and contextual output&lt;/h2&gt;&lt;p&gt;The second category of issues involved both technical display problems and behavioral output quality. When the agent returned Python objects (DataFrames, matplotlib figures, etc.), they weren't displaying properly in the Marimo chat interface. The agent would return a dictionary with these objects, but Marimo's chat UI doesn't automatically render matplotlib &lt;code&gt;Figure&lt;/code&gt; objects embedded in dictionaries.&lt;/p&gt;
&lt;p&gt;But there was a deeper behavioral problem: the agent was dumping DataFrames and plots without any explanatory text. I'd ask for a power analysis, and the agent would return a raw DataFrame with numbers -- no context, no interpretation, no explanation of what I was looking at. My sense of taste rebelled. This wasn't just a technical problem, it was an aesthetic one. I wanted a polished, consultant-like experience where explanatory text naturally flows between objects, not a data dump.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sometimes the best technical solutions come from caring about how things look and feel.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I solved both problems through a combination of technical infrastructure and prompt engineering. With AI assistance, I created a formatter system that processes dictionaries containing text and objects, converting strings to markdown and passing objects through for native display. This handled the technical display issue. It's implemented within &lt;code&gt;llamabot/components/formatters.py&lt;/code&gt; as &lt;code&gt;create_marimo_formatter&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But to solve the behavioral problem—getting the agent to actually provide contextual text—I had to update the system prompt. I added explicit guidance requiring that every object be preceded (and ideally followed) by explanatory text that connects back to the researcher's goals. The prompt now instructs the agent to create a dictionary with explanatory text strings interleaved with objects, then return it. The formatter processes this dictionary, creating that polished, consultant-like experience where text naturally flows between objects.&lt;/p&gt;
&lt;h2 id="failure-mode-3-agent-decision-making-and-domain-knowledge"&gt;Failure mode 3: Agent decision-making and domain knowledge&lt;/h2&gt;&lt;p&gt;The third category of failures involved the agent's decision-making process and understanding of domain conventions. Through systematic evaluation, I discovered several patterns.&lt;/p&gt;
&lt;p&gt;Here's a concrete example of how the prompt evolved. Initially, I had a simple instruction: "Ask clarifying questions about experiment goals, constraints, and assumptions." But the agent kept jumping straight to calculations. So I added:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Before (early version):&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Ask clarifying questions about experiment goals, constraints, and assumptions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;After (refined version):&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;CRITICAL - BE INQUISITIVE FIRST&lt;/strong&gt;: Before jumping into calculations, you MUST ask probing questions to understand the full context:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What effect size are they expecting or hoping to detect? Why?&lt;/li&gt;
&lt;li&gt;What is the expected variability in their measurements? Do they have pilot data?&lt;/li&gt;
&lt;li&gt;What are their practical constraints (budget, time, sample availability)?&lt;/li&gt;
&lt;li&gt;What are they most worried about with this experiment?&lt;/li&gt;
&lt;li&gt;Have they done similar experiments before? What issues did they encounter?&lt;/li&gt;
&lt;li&gt;What would make this experiment a "success" in their view?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Only after gathering this context&lt;/strong&gt; should you use &lt;code&gt;write_and_execute_code_wrapper&lt;/code&gt; to perform calculations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;The agent wasn't inquisitive enough.&lt;/strong&gt; Despite the system prompt emphasizing the need to ask questions, the agent would often jump straight to calculations without first understanding the researcher's context, constraints, and goals. I had to add multiple "CRITICAL" reminders in the prompt, explicitly stating that questioning should happen BEFORE calculations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent was trying to pass variable names as strings.&lt;/strong&gt; When the agent wanted to analyze a result from a previous tool call, it would try to write a function like &lt;code&gt;def analyze(mtt_power_analysis_result):&lt;/code&gt; and pass &lt;code&gt;{"mtt_power_analysis_result": "mtt_power_analysis_result"}&lt;/code&gt; - which passes the string literal, not the actual dictionary! I had to explicitly teach the agent to write functions with NO parameters that access variables directly from globals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Contradictions in the system prompt.&lt;/strong&gt; There were conflicting instructions about when to use &lt;code&gt;respond_to_user&lt;/code&gt; vs &lt;code&gt;return_object_to_user&lt;/code&gt;. I resolved this by clarifying: use &lt;code&gt;respond_to_user&lt;/code&gt; for text-only responses, and &lt;code&gt;return_object_to_user&lt;/code&gt; when you have Python objects to display.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent didn't understand domain-specific visualizations.&lt;/strong&gt; When the test user (a.k.a. me) asked for a "plate map" or "plate layout visualization," the agent would generate something, but it often wasn't what researchers expected.&lt;/p&gt;
&lt;p&gt;A plate map visualization in experimental biology is a very specific thing: a heatmap-style 8×12 grid (rows A-H, columns 1-12) where each well is color-coded by treatment group with a clear legend. Without explicit guidance, the agent would create generic bar charts or scatter plots that didn't match these domain conventions.&lt;/p&gt;
&lt;p&gt;I solved this by adding detailed specifications in the system prompt that describe exactly what plate map visualizations should look like, including the grid structure (rows labeled A-H, columns 1-12 for 96-well plates), color coding requirements (different colors per treatment, with a legend), complete code patterns showing how to create them with matplotlib, common plate formats (96-well, 384-well, 1536-well), and trigger phrases that should generate plate maps.&lt;/p&gt;
&lt;p&gt;This pattern - providing detailed specifications for domain-specific outputs - became a key strategy. When the agent needs to generate something that follows domain conventions, it needs explicit guidance on what those conventions are.&lt;/p&gt;
&lt;p&gt;Addressing these behavioral issues required multiple rounds of iteration. The system prompt didn't reach its final form in one go - as I discovered each issue, I added more explicit guidance, examples, and "CRITICAL" warnings. The prompt grew substantially through iterative refinement, and the agent's behavior improved dramatically with each iteration.&lt;/p&gt;
&lt;p&gt;The process wasn't linear during the early iteration phases - I'd fix one issue, test it, discover another, fix that, and sometimes realize the first fix needed refinement. Working with the Cursor coding agent helped me identify contradictions, explore multiple solution approaches, and get different perspectives before committing to changes. This iterative refinement process is essential when building domain-specific agents: you can't anticipate all the behavioral issues upfront, so you need to be prepared to evolve the prompt based on what you discover through testing.&lt;/p&gt;
&lt;h2 id="conclusion-key-lessons-for-building-domain-specific-agents"&gt;Conclusion: Key lessons for building domain-specific agents&lt;/h2&gt;&lt;p&gt;Building this AI statistics agent for experiment design revealed several important patterns that apply broadly to building domain-specific AI agents and LLM-powered tools:&lt;/p&gt;
&lt;h3 id="1-the-system-prompt-is-the-primary-control-surface"&gt;1. The system prompt is the primary control surface&lt;/h3&gt;&lt;p&gt;The agent's personality, decision-making process, inquisitiveness, and ability to provide contextual explanations are primarily controlled through prompt design rather than code changes. The prompt is where the domain knowledge lives, where the behavioral patterns are encoded, and where the "personality" of the agent is defined. Code provides the infrastructure, but the prompt provides the intelligence.&lt;/p&gt;
&lt;p&gt;If you want to change the agent's behavior, you're often better off modifying the prompt than changing the code. The prompt became a detailed instruction manual that teaches the agent not just &lt;em&gt;what&lt;/em&gt; to do, but &lt;em&gt;how&lt;/em&gt; to think, &lt;em&gt;when&lt;/em&gt; to ask questions, and &lt;em&gt;why&lt;/em&gt; certain patterns matter.&lt;/p&gt;
&lt;h3 id="2-testing-is-essential-but-not-sufficient"&gt;2. Testing is essential, but not sufficient&lt;/h3&gt;&lt;p&gt;Even with extensive testing, you cannot guarantee what will be seen in the real world. Users will ask questions you never thought of, use terminology you didn't anticipate, have edge cases in their data you didn't consider, and interact with the agent in ways that break your assumptions.&lt;/p&gt;
&lt;p&gt;This is why the agent is designed as a &lt;em&gt;consultant&lt;/em&gt; rather than an autonomous decision-maker. A human statistician still needs to review the work. The testing process is essential for building confidence, but you must also design the system with the assumption that it will encounter unexpected situations. This means implementing clear error handling, graceful degradation when things go wrong, explicit boundaries on what the agent can and cannot do, and human oversight for critical decisions.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final thoughts&lt;/h2&gt;&lt;p&gt;The process of building this agent revealed something I didn't expect: you can't anticipate all the behavioral issues upfront. The prompt grew from ~200 to ~600 lines through iterative discovery—each failure mode required explicit guidance I didn't know I'd need. Building domain-specific agents means being prepared to evolve your approach based on what you discover through testing, not just what you plan in advance.&lt;/p&gt;
&lt;h2 id="try-it-yourself"&gt;Try it yourself&lt;/h2&gt;&lt;p&gt;The experiment design agent is available as a Marimo notebook in the &lt;a href="https://github.com/ericmjl/llamabot"&gt;LlamaBot repository&lt;/a&gt;. You can run it locally with:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;git@github.com:ericmjl/llamabot.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llamabot/notebooks
uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;--watch&lt;span class="w"&gt; &lt;/span&gt;notebooks/experiment_design_agent.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The agent is designed to be inquisitive and consultative—it will ask probing questions about your experiment goals, constraints, and assumptions before providing recommendations. This AI statistics agent can help with power calculations, experimental design critique, sample data table generation, plate map visualizations, and biostatistical consultation for researchers in pharma and biotech.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Limitations&lt;/strong&gt;: This agent is a prototype focused on the experiment design phase. It's not a replacement for human statisticians—it's designed to amplify their knowledge and help researchers think through their designs before data collection. The agent requires human oversight and review, especially for high-stakes decisions. I haven't tested it across all experimental design types, and it may struggle with highly specialized domains or unusual constraints.&lt;/p&gt;
&lt;p&gt;If you're interested in building your own domain-specific agent, I hope the lessons and patterns shared here provide a useful starting point. The code is open source, and I welcome contributions and feedback.&lt;/p&gt;
&lt;p&gt;I'm working on a part 2 of this blog post, where I'll build out the statistical analysis agent—the companion to this experiment design agent. That post will cover how to build an agent that takes collected data and performs statistical analysis, model fitting, and interpretation.&lt;/p&gt;
&lt;p&gt;Thank you for reading this far. If you made it here, you've invested real time and attention in understanding not just what I built, but how and why—and that means a lot! Building this agent has been four months in the making, and sharing those discoveries with others who care about the same problems is what makes the work worthwhile. I'm grateful you came along for the ride!&lt;/p&gt;
</content></entry><entry><title>How to Reference Code Across Repositories with Coding Agents</title><link href="https://ericmjl.github.io/blog/2025/11/17/how-to-reference-code-across-repositories-with-coding-agents/" rel="alternate"/><updated>2025-11-17T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:438621db-6cf5-302d-b316-e2964a8f9e5a</id><content type="html">&lt;p&gt;I used to assume that coding agents like Cursor, GitHub Copilot, and Claude Code only work within a single workspace. This mental model led me to workarounds like copying files, creating complex multi-root workspace configurations, or constantly switching between projects.&lt;/p&gt;
&lt;p&gt;But coding agents can already read and write files from anywhere on your file system, not just the current workspace. The limitation wasn't in the tools; it was in my awareness of what they can do. You don't need to add folders to workspaces, create multi-root workspaces, or jump through configuration hoops. If you know where a repository lives on your disk, you can reference it directly.&lt;/p&gt;
&lt;h2 id="how-to-reference-code-from-other-repositories"&gt;How to reference code from other repositories&lt;/h2&gt;&lt;p&gt;The key is being explicit about file paths. Modern AI coding assistants like Cursor, GitHub Copilot, and Claude Code can access your entire file system, not just the current workspace. You just need to tell them where to look.&lt;/p&gt;
&lt;p&gt;I do most of my writing in an Obsidian vault, which isn't a Git repository; it's just a folder on disk. Sometimes I need to reference code from my LlamaBot repository, or other code repositories in which I am doing development. Instead of copying files or creating complex workspace configurations, I just tell the agent to read directly from the other directory.&lt;/p&gt;
&lt;p&gt;When I need the agent to understand something from LlamaBot, I can say "read the implementation from &lt;code&gt;~/github/llamabot/llamabot/bot/simplebot.py&lt;/code&gt;" and it works immediately. The key is being explicit with the path. You can also search by filename within a directory: "find the notebook named &lt;code&gt;pocketflow_testdrive.py&lt;/code&gt; in &lt;code&gt;~/github/llamabot&lt;/code&gt;". The agent reads the file directly from disk, no workspace configuration needed. You don't need to document paths anywhere; just reference them directly when you need them. That said, if you have commonly accessed paths, documenting them in &lt;code&gt;AGENTS.md&lt;/code&gt; can be helpful for quick reference.&lt;/p&gt;
&lt;p&gt;I used this method while writing my blog post &lt;a href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/"&gt;"How I Replaced 307 Lines of Agent Code with 4 Lines"&lt;/a&gt;. I was drafting the post in my Obsidian vault, but the actual code examples lived in a Marimo notebook within the LlamaBot repository. Rather than copying code snippets or switching workspaces, I had the agent read directly from &lt;code&gt;~/github/llamabot&lt;/code&gt; to pull in the exact implementation details I needed. This let me write about the code while staying in my writing environment, with the agent able to reference the actual source files to ensure accuracy.&lt;/p&gt;
&lt;h2 id="file-system-access-for-ai-coding-assistants-enables-this"&gt;File system access for AI coding assistants enables this&lt;/h2&gt;&lt;p&gt;Coding agents that have file system access can perform read and write operations anywhere they have permission. Tools like Cursor, GitHub Copilot, and Claude Code aren't restricted to the current workspace directory. This works because agents have access to shell tools, the most generic, text-based interface to computers. Shell commands produce text output that agents can read and understand, and they can execute commands anywhere on your system. This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You can reference code from any repository on your machine&lt;/li&gt;
&lt;li&gt;You can pull in documentation from other projects&lt;/li&gt;
&lt;li&gt;You can compare implementations across different codebases&lt;/li&gt;
&lt;li&gt;You can reference configuration files from related projects&lt;/li&gt;
&lt;li&gt;You can modify files across multiple repositories when needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The only requirement is that you know the path and can tell the agent where to look.&lt;/p&gt;
&lt;h2 id="common-scenarios"&gt;Common scenarios&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Blogging:&lt;/strong&gt; When writing blog posts about code, reference implementation details from your repositories. The agent can read the actual code to ensure accuracy, pulling in exact examples without copying files or switching workspaces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Architecture decisions:&lt;/strong&gt; Compare how similar problems are solved across different projects. The agent can read multiple implementations and help you understand trade-offs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Code reuse:&lt;/strong&gt; Before copying code, have the agent check if similar functionality exists elsewhere. It can read files from other repos to find existing solutions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Dependency understanding:&lt;/strong&gt; When working with a library you maintain, reference the library's source code directly. The agent can read implementation details to help you use it correctly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Cross-repository updates:&lt;/strong&gt; Update related files across multiple repositories simultaneously. For example, update documentation in one repo while modifying the implementation in another, or sync configuration changes across related projects.&lt;/p&gt;
&lt;h2 id="step-by-step-workflow-for-cross-repository-code-access"&gt;Step-by-step workflow for cross-repository code access&lt;/h2&gt;&lt;p&gt;The key trick is being explicit with paths or explicit instructions about how to get to files. Here's how to do it:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For repositories you already have cloned locally:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Reference the absolute path directly when asking the agent: "read &lt;code&gt;~/github/llamabot/src/llamabot/bot/simple.py&lt;/code&gt;"&lt;/li&gt;
&lt;li&gt;Or search by filename within a directory: "find the notebook named &lt;code&gt;pocketflow_testdrive.py&lt;/code&gt; in &lt;code&gt;~/github/llamabot&lt;/code&gt;"&lt;/li&gt;
&lt;li&gt;The agent reads the file immediately, no workspace configuration needed&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;For repositories you don't have locally:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Tell the agent exactly how to get to the file: "clone the repo &lt;code&gt;owner/repo&lt;/code&gt; into a temporary directory, then find the file at relative path &lt;code&gt;path/to/file.py&lt;/code&gt;"&lt;/li&gt;
&lt;li&gt;You can also specify a specific commit, branch, or tag: "clone the repo &lt;code&gt;owner/repo&lt;/code&gt; at commit &lt;code&gt;abc123&lt;/code&gt; into a temporary directory, then find the file at relative path &lt;code&gt;path/to/file.py&lt;/code&gt;" or "clone the repo &lt;code&gt;owner/repo&lt;/code&gt; and checkout branch &lt;code&gt;feature-branch&lt;/code&gt;, then find the file at relative path &lt;code&gt;path/to/file.py&lt;/code&gt;"&lt;/li&gt;
&lt;li&gt;The agent executes these commands using command line tools like &lt;code&gt;gh&lt;/code&gt; CLI or &lt;code&gt;git&lt;/code&gt;, reads what it needs, and can clean up the temporary clone when done&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No workspace management. No file copying. No complex configuration. Just explicit paths or explicit instructions. The agent needs clear direction on where to find files, whether that's an absolute path on your system or step-by-step instructions to clone and navigate to a file.&lt;/p&gt;
&lt;h2 id="common-questions-and-limitations"&gt;Common questions and limitations&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Do I need to configure workspace settings?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;No. Unlike traditional IDE workspace configurations, you don't need to add folders to workspaces or create multi-root setups. Just reference paths directly when you need them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do I manage paths for many repositories?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;You don't need to document them anywhere. Just reference paths directly when asking the agent to read files. If you find yourself referencing the same paths repeatedly, you can optionally document them in &lt;code&gt;AGENTS.md&lt;/code&gt; for convenience, but it's not required. You can also use MCP server prompts like &lt;code&gt;/remember&lt;/code&gt; (from my &lt;a href="https://github.com/ericmjl/ericmjl-productivity-mcp"&gt;personal productivity MCP server&lt;/a&gt;) to automatically capture frequently-used paths. The &lt;code&gt;/remember&lt;/code&gt; prompt reviews your conversation, identifies important learnings like repository paths, and adds timestamped entries to &lt;code&gt;AGENTS.md&lt;/code&gt; in the appropriate section.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Can agents modify files in other repositories?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, but be mindful. While agents can read and write files anywhere on your file system, it's easy to accidentally change files in other repositories. Use this capability deliberately rather than accidentally. Consider using read-only access for cross-repository references unless you specifically need to modify files.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What if I don't have the repository cloned locally?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Have the agent clone it temporarily using command line tools. The agent can use &lt;code&gt;gh&lt;/code&gt; CLI or &lt;code&gt;git&lt;/code&gt; commands to clone repositories into temporary directories, read what it needs, and clean up afterward.&lt;/p&gt;
&lt;h2 id="summary"&gt;Summary&lt;/h2&gt;&lt;p&gt;Thanks to shell tools, coding agents like Cursor, GitHub Copilot, and Claude Code aren't limited by workspace boundaries. They can access your entire file system for both reading and writing, so you can build workflows that span multiple projects without complex tooling.&lt;/p&gt;
&lt;p&gt;The simplicity is the point. You don't need special workspace configurations or multi-root setups. You just need to know where things live and tell the agent where to look. Reference paths directly, or have the agent clone repositories temporarily when needed.&lt;/p&gt;
&lt;p&gt;When you need to reference code from another repository, the agent can read it directly. Just point it to the path. This technique works with any AI coding assistant that has file system access, making it a universal solution for cross-repository code access.&lt;/p&gt;
</content></entry><entry><title>How I Replaced 307 Lines of Agent Code with 4 Lines</title><link href="https://ericmjl.github.io/blog/2025/11/16/how-i-replaced-307-lines-of-agent-code-with-4-lines/" rel="alternate"/><updated>2025-11-16T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:2e208dfe-9d34-3af0-9c37-4a21c5a8528a</id><content type="html">&lt;p&gt;I recently discovered &lt;a href="https://github.com/The-Pocket/PocketFlow?tab=readme-ov-file"&gt;PocketFlow&lt;/a&gt;, a framework for building LLM-enabled programs created by &lt;a href="https://zachary62.github.io/zach_public_material/"&gt;Zachary Huang&lt;/a&gt;. The entire framework is tiny—only 100 lines of code. What caught my attention is that PocketFlow takes a fundamentally different approach to LLM-powered programs, including Anthropic's &lt;a href="https://www.anthropic.com/engineering/building-effective-agents"&gt;workflows and agents&lt;/a&gt;, by structuring them as graphs.&lt;/p&gt;
&lt;p&gt;As someone who used graphs in my thesis work, &lt;a href="https://ericmjl.github.io/Network-Analysis-Made-Simple/"&gt;taught tutorials on applied graph theory&lt;/a&gt;, and &lt;a href="https://github.com/ericmjl/llamabot"&gt;builds my own agent frameworks&lt;/a&gt;, my curiosity was piqued. I wanted to see two things: whether I could learn enough of the framework to build something useful, and whether LlamaBot's abstractions could complement PocketFlow's approach.&lt;/p&gt;
&lt;p&gt;To explore this, I fired up a &lt;a href="https://marimo.io/"&gt;Marimo notebook&lt;/a&gt;. (You can fire it up too by running: &lt;code&gt;uvx marimo edit --sandbox &amp;lt;put URL here to notebook here&amp;gt;&lt;/code&gt;)&lt;/p&gt;
&lt;h2 id="understanding-the-core-nodes-and-flows"&gt;Understanding the Core - Nodes and Flows&lt;/h2&gt;&lt;p&gt;I started by building what I consider a "Hello World" program: a text topic extractor and question generator. This let me familiarize myself with PocketFlow's two core abstractions: &lt;code&gt;Nodes&lt;/code&gt; and &lt;code&gt;Flows&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;Node&lt;/code&gt; is a unit of execution structured like this:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SummarizeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...do stuff...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stuff_that_gets_passed_to_exec&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_res&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...do stuff...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;stuff_that_gets_passed_to_post&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...do stuff...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;string_indicator_what_to_do_next&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There's one more concept to introduce: &lt;code&gt;shared&lt;/code&gt;. In PocketFlow, &lt;code&gt;shared&lt;/code&gt; is like a big workspace that all &lt;code&gt;Node&lt;/code&gt;s can read and write from. Think of it as a kitchen island where chefs and cooks can grab ingredients and leave finished dishes. In computing terms, it's global state that programs can access. In practice, it's simply a dictionary that lives in memory, which any node can manipulate. For example, program &lt;code&gt;memory&lt;/code&gt; might be a key in there, implemented as a list.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;prep -&amp;gt; exec -&amp;gt; post&lt;/code&gt; design within a node is intentional. In theory, you could do everything in one step—there are no hooks that inject stuff between, say, &lt;code&gt;prep&lt;/code&gt; and &lt;code&gt;exec&lt;/code&gt;. In practice, doing everything in one step muddies the program and makes it harder to reason about. I'll show you why later in this post.&lt;/p&gt;
&lt;p&gt;Here's what each step is designed to do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;prep&lt;/code&gt;&lt;/strong&gt; takes stuff from the &lt;code&gt;shared&lt;/code&gt; dictionary, does any preprocessing, and passes it to &lt;code&gt;exec&lt;/code&gt;. This could include grabbing stuff from memory, interpolating it into a prompt, and returning it for execution with the LLM.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;exec&lt;/code&gt;&lt;/strong&gt; is where the bulk of heavy computation happens. We put API calls to LLM providers (Ollama, OpenAI, Anthropic, etc.) here. What gets returned is passed to the &lt;code&gt;post&lt;/code&gt; method.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;post&lt;/code&gt;&lt;/strong&gt; handles any post-processing. It receives &lt;code&gt;shared&lt;/code&gt;, &lt;code&gt;prep_res&lt;/code&gt; (the result of &lt;code&gt;prep&lt;/code&gt;), and &lt;code&gt;exec_res&lt;/code&gt; (result of &lt;code&gt;exec&lt;/code&gt;). The pattern I've settled on is archiving results in &lt;code&gt;shared&lt;/code&gt;—for example, storing execution results in memory. What gets returned by &lt;code&gt;post&lt;/code&gt; should be a string indicating which downstream path to follow. If nothing specific is needed, it returns &lt;code&gt;default&lt;/code&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A &lt;code&gt;Flow&lt;/code&gt; is declared with a starting &lt;code&gt;Node&lt;/code&gt; and follows the program until completion.&lt;/p&gt;
&lt;p&gt;With this abstraction, multiple LLM-powered abstractions and design patterns can be designed:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://github.com/the-pocket/.github/raw/main/assets/design.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;(Image from the PocketFlow official documentation.)&lt;/p&gt;
&lt;h2 id="example-1-topic-extractor-and-question-generator"&gt;Example 1 - Topic Extractor and Question Generator&lt;/h2&gt;&lt;p&gt;Here's how I built the two-step/node topic extractor + question generator. First, I declared the nodes:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExtractTopics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;First node: Extract key topics from input text&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;text_to_analyze&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;text_to_analyze&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;text_to_analyze&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;text_to_analyze&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;No content to analyze&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Extract 3-5 key topics from this text. Return only the topics as a comma-separated list:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;text_to_analyze&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;You are a helpful assistant that extracts key topics.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/qwen3:30b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;default&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;GenerateQuestions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Second node: Generate questions based on topics&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;txt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Cannot generate questions without valid topics&amp;quot;&lt;/span&gt;

        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Given these topics: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;topics&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;and the original text: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Generate 2 interesting questions for each topic.&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;You are a helpful assistant that generates thought-provoking questions.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/qwen3:30b&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;questions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;
        &lt;span class="c1"&gt;# No return statement since this is a terminal node.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, I declared the graph:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;extract_topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ExtractTopics&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;generate_questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GenerateQuestions&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;extract_topics&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;default&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;generate_questions&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The magic happens in this line:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;extract_topics&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;default&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;generate_questions&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This tells the flow that once the &lt;code&gt;extract_topics&lt;/code&gt; node emits &lt;code&gt;"default"&lt;/code&gt;, it should proceed to the &lt;code&gt;generate_questions&lt;/code&gt; node. The syntax is compact and looks exactly like an edge specification between two nodes.&lt;/p&gt;
&lt;p&gt;At this point, I deeply appreciate the clarity this approach forces upfront. When thinking about the flow as a graph, I'm forced to think about each node as a function that accepts inputs from shared state and returns a decision about what to do next. That decision can be deterministic (as above) or data-dependent (as we'll see below).&lt;/p&gt;
&lt;p&gt;Since GenAI can be viewed through the lens of automation, &lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;we should earn the privilege to use it&lt;/a&gt;. Automation requires a well-established process to be most effective. Framing a process in the language of graphs, inputs, and outputs—defining the process as a graph with carefully specified inputs and outputs, just like writing a computer program—is the clearest path to making automation work.&lt;/p&gt;
&lt;p&gt;Running the Flow looks like this:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;shared_topics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;txt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;two_node_flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;extract_topics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;two_node_flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared_topics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After running, we can inspect the &lt;code&gt;shared_topics&lt;/code&gt; dictionary to see our results:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;txt&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;topics&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;# added by ExtractTopics&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;questions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# added by GenerateQuestions&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One thing missing from PocketFlow is the ability to visualize the graph directly. Since the codebase was new to me, I sent a Cursor agent in the background to research and propose a solution. It came back with &lt;a href="https://github.com/ericmjl/llamabot/pull/279"&gt;this PR&lt;/a&gt;. Impressive!&lt;/p&gt;
&lt;p&gt;The Mermaid diagram for this workflow is:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
N1["ExtractTopics"]
N2["GenerateQuestions"]
N1 --&gt; N2
style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;

&lt;/pre&gt;&lt;h2 id="example-2-building-an-agent"&gt;Example 2 - Building an Agent&lt;/h2&gt;&lt;p&gt;Now, what if we want to build an agent?&lt;/p&gt;
&lt;p&gt;I'm going to work backwards here. My "hello world" test for agentic systems is making them tell me today's date. This works because an LLM will always hallucinate a date on its own, and that hallucination may or may not be correct. An agent that works properly should call a tool to get the actual date. The agent's graph should look like this:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
    N1["Decide"]
    N2["TodayDate"]
    N3["RespondToUser"]
    N1 --&gt;|"today_date"| N2
    N2 --&gt;|"decide"| N1
    N1 --&gt;|"respond_to_user"| N3
    style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
    style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
    style N3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;

&lt;/pre&gt;&lt;p&gt;I consider this a "Hello World" agent because a failing agent will skip straight to &lt;code&gt;respond_to_user&lt;/code&gt; when asked for today's date, without first calling &lt;code&gt;today_date&lt;/code&gt; to get the actual information.&lt;/p&gt;
&lt;p&gt;To build this agent, I need three nodes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;Decide&lt;/code&gt;&lt;/strong&gt;: Uses an LLM to decide which tool to call next, given the prompt&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;TodayDate&lt;/code&gt;&lt;/strong&gt;: Executes without LLMs and returns today's date in the current timezone&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;RespondToUser&lt;/code&gt;&lt;/strong&gt;: Responds to the user with the appropriate context&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's how I wrote them. First, the &lt;code&gt;Decide&lt;/code&gt; node:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;llamabot.components.tools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;respond_to_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;search_internet&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pydantic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;

&lt;span class="n"&gt;search_internet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;search_internet&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;respond_to_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ToolChoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The name of the tool to use&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;system&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;decision_bot_system_prompt&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Given the chat history, pick for me one or more tools to execute&lt;/span&gt;
&lt;span class="sd"&gt;    in order to satisfy the user&amp;#39;s query.&lt;/span&gt;

&lt;span class="sd"&gt;    Give me just the tool name to pick.&lt;/span&gt;
&lt;span class="sd"&gt;    Use the tools judiciously to help answer the user&amp;#39;s query.&lt;/span&gt;
&lt;span class="sd"&gt;    Query is always related to one of the tools.&lt;/span&gt;
&lt;span class="sd"&gt;    Use respond_to_user if you have enough information to answer the original query.&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Decide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;query&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pydantic_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ToolChoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decision_bot_system_prompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Chosen Tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The key thing to note is that we inject the available tools into the system prompt of the tool-selecting agent.&lt;/p&gt;
&lt;p&gt;Next, the &lt;code&gt;TodayDate&lt;/code&gt; node:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TodayDate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Today&amp;#39;s date: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And finally, the &lt;code&gt;RespondToUser&lt;/code&gt; node:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;RespondToUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The response to the user.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s2"&gt;&amp;quot;You are a helpful assistant.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/gemma3n:latest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pydantic_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, we set up the graph:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Set up the graph&lt;/span&gt;
&lt;span class="n"&gt;today__date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TodayDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;respond__to__user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RespondToUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Decide&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;shared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What is the date today?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;today_date&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;today__date&lt;/span&gt;
&lt;span class="n"&gt;today__date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;decide&lt;/span&gt;
&lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;respond_to_user&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;respond__to__user&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I used &lt;code&gt;__&lt;/code&gt; in the node names to avoid clashing with the original functions.&lt;/p&gt;
&lt;p&gt;Then we run it:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;flow2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;flow2&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Take my word for it (or check out the notebook yourself)—it reliably gives me today's date.&lt;/p&gt;
&lt;h2 id="example-3-agent-with-shell-commands"&gt;Example 3 - Agent with Shell Commands&lt;/h2&gt;&lt;p&gt;To push things further, I tried a tool that needs arguments. A good "hello world" for this is executing shell commands in response to questions like, "What's in this folder?"&lt;/p&gt;
&lt;p&gt;For this, I created a second version of the &lt;code&gt;Decide&lt;/code&gt; node called &lt;code&gt;Decide2&lt;/code&gt;, where I instantiate and execute the &lt;code&gt;ToolChoice&lt;/code&gt; and tool selection &lt;code&gt;StructuredBot&lt;/code&gt; within &lt;code&gt;exec&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Decide2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;sysprompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;decision_bot_system_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sysprompt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ToolChoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tools&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The name of the tool to use&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;justification&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Why this tool was chosen.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pydantic_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ToolChoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decision_bot_system_prompt&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Query: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Chosen Tool: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I then created a &lt;code&gt;ShellCommand&lt;/code&gt; node that uses the same pattern—leveraging &lt;code&gt;StructuredBot&lt;/code&gt; for structured generation to constrain the LLM's output to exactly what I need:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ShellCommand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Cmd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;The shell command to execute&amp;quot;&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;You are an expert at writing shell commands. For the chat trace that you will be given, write a shell command that accomplishes the user&amp;#39;s request. Only output the command, nothing else.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pydantic_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/gemma3n:latest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;execute_shell_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Output: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exec_result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, we set up the graph:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Set up the graph&lt;/span&gt;
    &lt;span class="n"&gt;today_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TodayDate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;respond_to_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;RespondToUser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Decide2&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;shell_command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ShellCommand&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;today_date&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;today_date&lt;/span&gt;
    &lt;span class="n"&gt;today_date&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;decide&lt;/span&gt;
    &lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;execute_shell_command&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;shell_command&lt;/span&gt;
    &lt;span class="n"&gt;shell_command&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;decide&lt;/span&gt;
    &lt;span class="n"&gt;decide&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;respond_to_user&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;respond_to_user&lt;/span&gt;

    &lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;decide&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;flow&lt;/span&gt;


&lt;span class="n"&gt;flow3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The graph would look like this:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
N1["Decide2"]
N2["TodayDate"]
N3["ShellCommand"]
N4["RespondToUser"]
N1 --&gt;|"today_date"| N2
N2 --&gt;|"decide"| N1
N1 --&gt;|"execute_shell_command"| N3
N3 --&gt;|"decide"| N1
N1 --&gt;|"respond_to_user"| N4
style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N4 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;

&lt;/pre&gt;&lt;p&gt;I wrapped it in a &lt;code&gt;_()&lt;/code&gt; function to protect the globally scoped variables in the Marimo notebook. Note that I included &lt;code&gt;today_date&lt;/code&gt; as well, just to "pollute" the namespace and make it more challenging when asking shell-related questions. When we interact with the agent:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;shared3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;shared3&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;What in my current working directory?&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;shared3&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;shared3&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tools&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;respond_to_user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;execute_shell_command&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;flow3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shared3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It calls on &lt;code&gt;shell_command&lt;/code&gt;, and gives me back this response:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Okay, here&amp;#39;s a list of the files and directories in your current working directory:

*   **Directories:**
    *   `__marimo__`
    *   `.` (current directory)
    *   `..` (parent directory)

*   **Files:**
    *   `agentbot_build.py`
    *   `agents.py`
    *   `chatbot_as_agent.py`
    *   `conversation-threads.py`
    *   `data.csv`
    *   `ic50_data_with_confounders.csv`
    *   `intro.py`
    *   `lancedb_docstore.py`
    *   `pocketflow_testdrive.py`
    *   `react-agentbot-demo.py`
    *   `README.md`
    *   `toolbot_chatdata.py`
    *   `tools.py`

That&amp;#39;s a total of 17 files and directories. Let me know if you&amp;#39;d like more details about any of them!
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I was also able to ask "Hey, what files have been modified today?" and the agent successfully executed the appropriate shell command.&lt;/p&gt;
&lt;p&gt;Effectively, this pattern is nothing more than a coordinating agent/LLM delegating work to specialized tools.&lt;/p&gt;
&lt;h2 id="rewriting-agentbot-with-pocketflow"&gt;Rewriting AgentBot with PocketFlow&lt;/h2&gt;&lt;p&gt;Finally, I decided to take what I'd learned and redo the &lt;code&gt;AgentBot&lt;/code&gt; implementation in LlamaBot. My previous implementation (version 0.16.3) was messy—the &lt;code&gt;__call__&lt;/code&gt; method alone was 307 lines with a while-loop, maximum tries, ThreadPoolExecutor for parallel tool execution, tool call caching, and extensive metadata tracking. PocketFlow had a better abstraction for the agentic loop: a &lt;code&gt;Flow&lt;/code&gt; state machine following edges on a graph. I thought I could redesign &lt;code&gt;AgentBot&lt;/code&gt; to take advantage of this pattern.&lt;/p&gt;
&lt;p&gt;The rewrite involved some really interesting patterns. I completely replaced the ReAct (Reasoning and Acting) loop with PocketFlow's graph-based tool orchestration. This shifts from an iterative loop-based approach to a declarative graph-based one, where tool execution flows through a directed graph rather than a sequential loop.&lt;/p&gt;
&lt;p&gt;The implementation centers on three key abstractions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. The &lt;code&gt;@nodeify&lt;/code&gt; decorator&lt;/strong&gt; transforms any callable function into a PocketFlow Node. It wraps functions with PocketFlow's Node interface, implementing the required &lt;code&gt;prep&lt;/code&gt;, &lt;code&gt;exec&lt;/code&gt;, and &lt;code&gt;post&lt;/code&gt; methods. The tricky part is that &lt;code&gt;@nodeify&lt;/code&gt; needs to preserve access to the underlying function's metadata—particularly the &lt;code&gt;json_schema&lt;/code&gt; attribute added by the &lt;code&gt;@tool&lt;/code&gt; decorator—through attribute proxying, so ToolBot can discover and use tools even after they've been wrapped as nodes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2. The &lt;code&gt;DecideNode&lt;/code&gt;&lt;/strong&gt; encapsulates the decision-making logic. This node uses ToolBot internally to analyze the conversation history stored in shared state and select which tool to execute next. It expects a shared state dictionary with a &lt;code&gt;"memory"&lt;/code&gt; key containing the conversation history as a list of strings. When executed, it calls ToolBot with this memory, extracts the first tool call from ToolBot's response, parses the JSON-formatted arguments, and stores them in &lt;code&gt;shared["func_call"]&lt;/code&gt; for the next node. The node then returns the tool name as a routing action, which PocketFlow uses to navigate the graph.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;3. Flow graph construction&lt;/strong&gt; happens at initialization time. AgentBot automatically wraps all provided tools (plus default tools like &lt;code&gt;today_date&lt;/code&gt; and &lt;code&gt;respond_to_user&lt;/code&gt;) with both &lt;code&gt;@tool&lt;/code&gt; and &lt;code&gt;@nodeify&lt;/code&gt; decorators, then builds bidirectional connections: from the decide node to each tool node (using the tool's function name as the action), and from each tool node back to the decide node (except for terminal tools like &lt;code&gt;respond_to_user&lt;/code&gt; that have &lt;code&gt;loopback_name=None&lt;/code&gt;). This creates a graph where execution can flow from decision to tool and back to decision, enabling multi-step reasoning.&lt;/p&gt;
&lt;p&gt;A few technical requirements make this work: tools need type annotations (for JSON schema generation), the shared state needs a &lt;code&gt;"memory"&lt;/code&gt; list for conversation history, and tool arguments are passed through &lt;code&gt;shared["func_call"]&lt;/code&gt;. The DecideNode selects one tool at a time, and tools are stateless—they get fresh arguments each call and communicate through memory.&lt;/p&gt;
&lt;p&gt;What's remarkable about this implementation is how compact it is. The &lt;code&gt;@nodeify&lt;/code&gt; decorator is just 100 lines, and most of that is documentation. The core logic is elegant:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;nodeify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loopback_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FuncNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loopback_name&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;

            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;prep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;

            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;func_call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;func_call&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;func_call&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prep_result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exec_res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;exec_res&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt;

            &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# Proxy to original function for json_schema access&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;func&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;FuncNode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;decorator&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The entire AgentBot class is similarly compact—about 100 lines total. Compare this to the previous implementation where the &lt;code&gt;__call__&lt;/code&gt; method alone was 307 lines, with complex while loop logic, tool call caching, parallel execution via ThreadPoolExecutor, and extensive state management:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Old implementation (v0.16.3): 307-line __call__ method&lt;/span&gt;
&lt;span class="c1"&gt;# Plus 50-line caching wrapper, 21-line execution helper&lt;/span&gt;
&lt;span class="c1"&gt;# Total: 378 lines of orchestration code&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;iteration&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_iterations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Call model with tools&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;completion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;raw_messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;tool_choice&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;auto&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# Execute tools in parallel with caching&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_execute_tool_with_cache&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="c1"&gt;# Handle results, update messages, manage cache,&lt;/span&gt;
                &lt;span class="c1"&gt;# track metadata, handle errors...&lt;/span&gt;
                &lt;span class="o"&gt;...&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="c1"&gt;# Handle finalization, memory updates, logging, metrics...&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The new implementation replaces all of that with a simple graph construction:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# New implementation: ~100 lines total, declarative graph&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AgentBot&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;decide_node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;gpt-4.1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ... validation and setup ...&lt;/span&gt;

        &lt;span class="c1"&gt;# Build PocketFlow graph: connect tools to decide node&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide_node&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;tool_node&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide_node&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Flow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide_node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;memory&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shared&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;result&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Full implementation includes validation, tool wrapping, and state management—about 100 lines total vs 307+ for the old &lt;code&gt;__call__&lt;/code&gt; method alone.&lt;/p&gt;
&lt;h3 id="the-magic-of-building-an-agent-in-just-4-lines"&gt;The Magic of Building an Agent in Just 4 Lines&lt;/h3&gt;&lt;p&gt;The most remarkable part of this implementation is how the entire agent graph is constructed. Look at these four lines carefully:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide_node&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tool_name&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tool_node&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;tool_node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decide_node&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;This is it.&lt;/strong&gt; This is the entire graph construction that turns a collection of tools into a working agent. Let me break down what's happening:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Line 1&lt;/strong&gt;: Loop through each tool&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line 2&lt;/strong&gt;: Connect the decide node to the tool node—when the LLM chooses this tool, execution flows to it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line 3&lt;/strong&gt;: Check if this tool should loop back (terminal tools like &lt;code&gt;respond_to_user&lt;/code&gt; have &lt;code&gt;loopback_name=None&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Line 4&lt;/strong&gt;: Connect the tool back to the decide node—after execution, control returns to decision-making&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That's the entire agent architecture. Four lines. The &lt;code&gt;- "action" &amp;gt;&amp;gt;&lt;/code&gt; syntax creates directed edges in the graph, and PocketFlow handles all the state management, routing, and execution orchestration. Compare this to the 307-line &lt;code&gt;__call__&lt;/code&gt; method in the previous implementation (version 0.16.3) with its complex loop-based logic, thread pools, state tracking, and termination conditions.&lt;/p&gt;
&lt;p&gt;This is what I mean by "graph-based thinking" being clearer—the entire execution flow is explicit and declarative. You can see at a glance how decisions flow to tools and back to decisions, enabling multi-step reasoning.&lt;/p&gt;
&lt;p&gt;The difference is striking. The old implementation required manual loop management, explicit state tracking, parallel execution coordination, and complex termination logic. The new implementation declares the graph structure once, and PocketFlow handles all the execution details.&lt;/p&gt;
&lt;p&gt;This graph-based approach provides several advantages. The flow graph is constructed once at initialization, making the execution path explicit and visualizable—you can render the agent's decision flow as a Mermaid diagram using the visualization feature I added to LlamaBot. The separation of concerns is clearer: decision-making lives in &lt;code&gt;DecideNode&lt;/code&gt;, tool execution in wrapped function nodes, and orchestration in PocketFlow's flow engine. The implementation is also more modular—you can swap out the decision node or customize tool wrapping behavior without rewriting the core agent logic. Finally, by leveraging PocketFlow's graph execution model, we gain access to its execution capabilities and potential future extensions for parallel execution or conditional routing.&lt;/p&gt;
&lt;h2 id="visualizing-different-agent-architectures"&gt;Visualizing Different Agent Architectures&lt;/h2&gt;&lt;p&gt;One really cool feature I added to LlamaBot is the ability to visualize any agent's graph structure using Mermaid diagrams. The &lt;code&gt;AgentBot._display_()&lt;/code&gt; method automatically renders the flow graph, making it easy to see how different tool configurations create different architectures.&lt;/p&gt;
&lt;p&gt;Here's a simple agent with just two tools:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;llamabot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentBot&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;llamabot.components.tools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;llamabot.components.pocketflow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;nodeify&lt;/span&gt;

&lt;span class="nd"&gt;@nodeify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Search the web for information.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;web_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AgentBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_display_&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Renders Mermaid diagram in Marimo&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The resulting graph shows the decision node connected to &lt;code&gt;today_date&lt;/code&gt;, &lt;code&gt;search_web&lt;/code&gt;, and &lt;code&gt;respond_to_user&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
N1["DecideNode"]
N2["today_date"]
N3["search_web"]
N4["respond_to_user"]
N1 --&gt;|"today_date"| N2
N2 --&gt;|"decide"| N1
N1 --&gt;|"search_web"| N3
N3 --&gt;|"decide"| N1
N1 --&gt;|"respond_to_user"| N4
style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N4 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
&lt;/pre&gt;&lt;p&gt;Add more tools, and the graph automatically expands. Here's an agent with code execution and file operations:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nd"&gt;@nodeify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_and_execute_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;dependencies_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;python_version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;gt;=3.11&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Write and execute a Python script in a secure Docker sandbox.&lt;/span&gt;

&lt;span class="sd"&gt;    :param code: The Python code to execute&lt;/span&gt;
&lt;span class="sd"&gt;    :param dependencies_str: Comma-separated pip dependencies&lt;/span&gt;
&lt;span class="sd"&gt;    :param python_version: Python version requirement&lt;/span&gt;
&lt;span class="sd"&gt;    :return: Dictionary with stdout, stderr, and status&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="c1"&gt;# Uses ScriptExecutor to run code in isolated Docker container&lt;/span&gt;
    &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ScriptExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_script&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;stdout&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stdout&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;stderr&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stderr&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;status&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;@nodeify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Read and return file contents.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filepath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AgentBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;search_web&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_and_execute_script&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read_file&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The graph now shows six tool nodes all connected bidirectionally to the decision node (except terminal tools):&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
N1["DecideNode"]
N2["today_date"]
N3["search_web"]
N4["write_and_execute_script"]
N5["read_file"]
N6["respond_to_user"]
N7["return_object_to_user"]
N1 --&gt;|"today_date"| N2
N2 --&gt;|"decide"| N1
N1 --&gt;|"search_web"| N3
N3 --&gt;|"decide"| N1
N1 --&gt;|"write_and_execute_script"| N4
N4 --&gt;|"decide"| N1
N1 --&gt;|"read_file"| N5
N5 --&gt;|"decide"| N1
N1 --&gt;|"respond_to_user"| N6
N1 --&gt;|"return_object_to_user"| N7
style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N4 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N5 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N6 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N7 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
&lt;/pre&gt;&lt;p&gt;What I love about this is how the graph makes it immediately obvious what capabilities an agent has. You can see at a glance which tools are available, understand the control flow, and reason about how the agent will behave. The visualization transforms the abstract "agent with tools" into a concrete, inspectable structure.&lt;/p&gt;
&lt;p&gt;Here's a real-world example—an experiment design agent I built for critiquing statistical experiment designs:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nd"&gt;@nodeify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loopback_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;decide&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@tool&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;critique_experiment_design&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;design&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Critique an experiment design and identify potential flaws,&lt;/span&gt;
&lt;span class="sd"&gt;    biases, or weaknesses.&lt;/span&gt;

&lt;span class="sd"&gt;    :param design: Description of the proposed experiment design&lt;/span&gt;
&lt;span class="sd"&gt;    :return: Critique with identified issues and suggestions&lt;/span&gt;
&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SimpleBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;experiment_design_critique_sysprompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;design&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AgentBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;critique_experiment_design&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;write_and_execute_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;globals&lt;/span&gt;&lt;span class="p"&gt;())]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This agent has a specialized domain focus. The graph shows all its capabilities, including the default tools that every &lt;code&gt;AgentBot&lt;/code&gt; gets automatically:&lt;/p&gt;
&lt;pre class="mermaid"&gt;
graph LR
N1["DecideNode"]
N2["today_date"]
N3["critique_experiment_design"]
N4["write_and_execute_code"]
N5["respond_to_user"]
N6["return_object_to_user"]
N1 --&gt;|"today_date"| N2
N2 --&gt;|"decide"| N1
N1 --&gt;|"critique_experiment_design"| N3
N3 --&gt;|"decide"| N1
N1 --&gt;|"write_and_execute_code"| N4
N4 --&gt;|"decide"| N1
N1 --&gt;|"respond_to_user"| N5
N1 --&gt;|"return_object_to_user"| N6
style N1 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N2 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N3 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N4 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N5 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
style N6 fill:#e1f5ff,stroke:#01579b,stroke-width:2px;
&lt;/pre&gt;&lt;p&gt;Notice that &lt;code&gt;today_date&lt;/code&gt;, &lt;code&gt;respond_to_user&lt;/code&gt;, and &lt;code&gt;return_object_to_user&lt;/code&gt; are included by default in every &lt;code&gt;AgentBot&lt;/code&gt;. The graph immediately tells you this agent can critique designs, execute code to analyze data, and return Python objects directly to the user—but it's not a general-purpose assistant. It's specialized for experiment design evaluation. The visual structure encodes the agent's purpose.&lt;/p&gt;
&lt;p&gt;This is only possible because of the graph-based architecture. With the old loop-based implementation, there was no clean way to visualize the execution flow—it was hidden inside imperative control logic.&lt;/p&gt;
&lt;h2 id="what-i-learned"&gt;What I Learned&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Externalize memory as shared state&lt;/strong&gt;. Memory lives in the &lt;code&gt;shared&lt;/code&gt; dictionary that all nodes can access, rather than being intrinsic to each bot. We just feed memory context in each time a node executes. This has good economics—if you have prompt caching on the API provider's side, simply appending to an ever-growing memory is a great way to take advantage of pre-computed neural network outputs from previous runs. I used to think of memory as &lt;em&gt;intrinsic&lt;/em&gt; to a bot, but I've changed my mind: allowing multiple bots to share access to the same memory is a useful simplification, even if it's not suitable for every circumstance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The &lt;code&gt;prep -&amp;gt; exec -&amp;gt; post&lt;/code&gt; pattern&lt;/strong&gt; is worth adhering to. I found myself appending to memory in &lt;code&gt;post&lt;/code&gt; after doing the &lt;code&gt;exec&lt;/code&gt;ution. &lt;code&gt;prep&lt;/code&gt; turns out to be useful for preprocessing user inputs or manipulating memory as needed. The overall effect is that it's much easier to &lt;strong&gt;unit test or set up evals&lt;/strong&gt; for individual nodes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PocketFlow's graph abstraction brings clarity&lt;/strong&gt;. The analogy of LLM agents (&lt;code&gt;Node&lt;/code&gt;s) as chefs/cooks accessing a kitchen island's worth of things (&lt;code&gt;shared&lt;/code&gt;) is a powerful contrast to my previous loop-based approach in LlamaBot, where I manually tracked state, managed iterations, and coordinated tool execution. This insight is exactly why I rewrote AgentBot to use this graph-based architecture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A wide variety of LLM-powered architectures&lt;/strong&gt; can be built with just &lt;code&gt;Node&lt;/code&gt;s and &lt;code&gt;Flow&lt;/code&gt;s. Most LLM applications I've built—whether for myself or for others—have not been "agentic" but more like "workflows." These are what some might consider boring. Yet they are high ROI precisely because they take repetitive and boring work out of our hands! PocketFlow gives us a way to express flows as graphs, effectively state machines whose actions are either fully deterministic or determined by an LLM's choice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PocketFlow is minimalistic&lt;/strong&gt;, which offloads a lot of heavy lifting when working with LLMs. The flexibility is both a strength and a weakness: great for power users, but potentially intimidating for newcomers. I found it easiest to rely heavily on &lt;code&gt;StructuredBot&lt;/code&gt; to output decisions made by the LLM. Structured generation is, generally speaking, the most useful abstraction in the LLM world that I keep turning back to.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The agent pattern is everywhere once you recognize it&lt;/strong&gt;. While writing this post, I realized that the agentic coding IDEs we've gotten used to—tools like Cursor, GitHub Copilot, and others—follow the exact same pattern I've been describing. They have a decision node that analyzes your code and context, tool nodes for reading files, searching codebases, editing code, and responding to you. The flow is the same: decide what to do, execute a tool, update context, decide again. Understanding this pattern in PocketFlow helped me see it operating in the tools I use every day. The abstraction is the mental model that makes sense of how modern AI-powered tools work.&lt;/p&gt;
&lt;p&gt;The biggest lesson? &lt;strong&gt;Thinking in graphs transforms how you build LLM programs&lt;/strong&gt;. The shift from imperative loops to declarative graphs means you declare &lt;em&gt;what&lt;/em&gt; should happen instead of specifying &lt;em&gt;how&lt;/em&gt; to execute step-by-step. This brings clarity, modularity, and makes your execution flow explicit. Whether you're building simple workflows or complex agents, representing them as graphs forces you to think clearly about state, decisions, and flow. That mental model shift has changed how I approach every LLM application I build.&lt;/p&gt;
</content></entry><entry><title>Safe ways to let your coding agent work autonomously</title><link href="https://ericmjl.github.io/blog/2025/11/8/safe-ways-to-let-your-coding-agent-work-autonomously/" rel="alternate"/><updated>2025-11-08T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0da86835-0adf-31d4-be7c-70ec5f74e11d</id><content type="html">&lt;p&gt;Coding agents promise to unlock significant productivity gains by working autonomously in the background—gathering context, running tests, searching documentation, and making progress on tasks without constant human intervention. The more autonomous they become, the more value they deliver. Yet this autonomy creates a fundamental tension: we need agents to act independently to realize their potential, but we must prevent them from taking irreversible actions we don't want.&lt;/p&gt;
&lt;p&gt;This tension became painfully clear when I asked Comet, an agentic browser, "how to archive repo" in the same casual way I'd ask Google. The agent interpreted this as a direct command and archived my LlamaBot repository. What I wanted was information; what I got was an unintended action with real consequences.&lt;/p&gt;
&lt;p&gt;The problem isn't unique to Comet. Any coding agent with sufficient autonomy can make destructive changes: deleting files, force-pushing to main, committing broken code, or modifying critical configurations. We need safeguards that allow agents to work freely on safe operations while blocking potentially harmful actions. The solution lies in configuring your development environment with intelligent boundaries—auto-approving read-only commands while requiring explicit approval for anything that modifies state.&lt;/p&gt;
&lt;h2 id="auto-approve-safe-command-line-commands"&gt;Auto-approve safe command line commands&lt;/h2&gt;&lt;p&gt;The foundation of autonomous coding agent operation is allowing certain command line commands to run without manual approval. Commands like &lt;code&gt;grep&lt;/code&gt;/&lt;code&gt;ripgrep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;/&lt;code&gt;fd&lt;/code&gt;, &lt;code&gt;pixi run pytest...&lt;/code&gt;, and similar read-only or context-gathering operations enable LLM agents to autonomously understand codebases and test suites. For CLI tools that interact with external services, I also auto-approve &lt;code&gt;gh pr view&lt;/code&gt;, which allows the agent to gather context from GitHub pull requests while working in the background.&lt;/p&gt;
&lt;p&gt;The critical rule: &lt;strong&gt;only auto-accept commands that are non-destructive&lt;/strong&gt;. Never auto-approve &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt;, &lt;code&gt;rm&lt;/code&gt;, or other filesystem, git, or state-modifying changes. This creates a safe boundary where agents can explore and learn, but cannot make irreversible changes without your explicit approval.&lt;/p&gt;
&lt;p&gt;Here's my mental model for categorizing commands:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Safe to auto-approve:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read operations: &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, &lt;code&gt;less&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Code analysis: &lt;code&gt;pytest&lt;/code&gt; (read-only test runs), &lt;code&gt;mypy&lt;/code&gt;, &lt;code&gt;ruff check&lt;/code&gt; (without &lt;code&gt;--fix&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Context gathering: &lt;code&gt;gh pr view&lt;/code&gt;, &lt;code&gt;gh issue view&lt;/code&gt;, &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git diff&lt;/code&gt;, &lt;code&gt;git show&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Package managers (read-only): &lt;code&gt;pip list&lt;/code&gt;, &lt;code&gt;npm list&lt;/code&gt;, &lt;code&gt;cargo tree&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Documentation build: &lt;code&gt;mkdocs serve&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Never auto-approve:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;File system mutations: &lt;code&gt;rm&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;touch&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Git writes: &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt;, &lt;code&gt;git reset&lt;/code&gt;, &lt;code&gt;git checkout -b&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Package installs: &lt;code&gt;pixi add&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The edge cases are where it gets interesting. I auto-approve &lt;code&gt;pytest&lt;/code&gt; because test runs are read-only, but I require approval for any command that modifies files, even if it's technically reversible. The key distinction is whether a command changes state: &lt;code&gt;git status&lt;/code&gt; and &lt;code&gt;git diff&lt;/code&gt; are safe because they're pure reads, while &lt;code&gt;git commit&lt;/code&gt; and &lt;code&gt;git push&lt;/code&gt; modify repository state and require explicit approval. &lt;code&gt;git add&lt;/code&gt; is a bit of a gray area, but I am ok with auto-approving it since it's technically reversible, and because coding agents are often much faster than I could be at selectively adding files to the staging area.&lt;/p&gt;
&lt;h2 id="enable-automatic-web-search"&gt;Enable automatic web search&lt;/h2&gt;&lt;p&gt;For Cursor and Claude Code, automatic web &lt;em&gt;search&lt;/em&gt; without approval requests is another powerful capability. I have web search auto-approved on my machine, which allows agents to look up documentation, error messages, and solutions independently. This is particularly valuable when agents encounter unfamiliar error messages or need to check current API documentation that may have changed since the model's training cutoff.&lt;/p&gt;
&lt;p&gt;However, I monitor outputs for prompt poisoning, since internet-based prompt poisoning is a known attack vector for AI systems. The risk is that malicious content from web searches could influence the agent's behavior in subsequent actions. I've found this risk manageable for coding tasks, but I'm more cautious with agents that have broader system access or handle sensitive data.&lt;/p&gt;
&lt;h2 id="know-your-emergency-stop-shortcuts"&gt;Know your emergency stop shortcuts&lt;/h2&gt;&lt;p&gt;Every coding agent platform provides keyboard shortcuts to cancel actions in progress. These are essential when you notice an agent looping, going down an unproductive path, or making changes you don't want:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cursor: &lt;code&gt;Ctrl+C&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;VSCode + GitHub Copilot: &lt;code&gt;Cmd+Esc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Claude Code: &lt;code&gt;Esc&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're monitoring the agent's activity, these shortcuts let you intervene immediately when something goes wrong.&lt;/p&gt;
&lt;h2 id="correct-agent-behavior-in-real-time"&gt;Correct agent behavior in real-time&lt;/h2&gt;&lt;p&gt;When you catch an agent doing something undesirable, stop it immediately, then redirect it. I instruct agents to record corrections in &lt;code&gt;AGENTS.md&lt;/code&gt; and continue with the updated guidance. An example prompt:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;No, I don&amp;#39;t want you to do &amp;lt;thing&amp;gt;. Instead, you should do &amp;lt;a different thing&amp;gt;. Record this in AGENTS.md, and then continue what you were doing.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This approach creates a persistent record of preferences that improves future agent behavior. The &lt;code&gt;AGENTS.md&lt;/code&gt; file becomes a living document of your development standards and preferences, which agents can reference in future sessions. I've implemented this pattern in my &lt;a href="https://github.com/ericmjl/ericmjl-productivity-mcp"&gt;personal productivity MCP server&lt;/a&gt;, which provides a standardized way to store and retrieve these preferences across different agent platforms.&lt;/p&gt;
&lt;h2 id="write-prescriptive-prompts-for-complex-tasks"&gt;Write prescriptive prompts for complex tasks&lt;/h2&gt;&lt;p&gt;I created the personal productivity MCP server to help me take my favourite prompts from system to system. MCP (Model Context Protocol) servers provide a standardized way to expose tools and context to AI agents across different platforms. One thing I learned from my colleague Anand Murthy about how to write such prompts is to be extremely prescriptive about the actions and tools that I want the agent to use.&lt;/p&gt;
&lt;p&gt;Generic prompts like "help me debug this GitHub Actions workflow" leave too much room for interpretation. Instead, specify exact commands, tools, and steps. For example, if I'm looking to debug a GitHub Actions issue, the prompt that I have looks like this:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;You are helping me debug a failed GitHub Actions workflow. Follow these steps to systematically analyze and resolve the issue:

1. **Extract workflow information**: Parse the provided URL to identify:
   - Repository owner and name
   - Workflow run ID
   - Workflow name
   - Branch/commit that triggered the run

2. **Fetch workflow logs using GitHub CLI**:
   - Use `gh run list` to verify the workflow run exists
   - Use `gh run view &amp;lt;run-id&amp;gt;` to get detailed run information
   - Use `gh run view &amp;lt;run-id&amp;gt; --log` to download and display the full logs
   - Use `gh run view &amp;lt;run-id&amp;gt; --log-failed` to focus on failed job logs

3. **Analyze the failure**:
   - Identify which job(s) failed and at what step
   - Look for error messages, exit codes, and stack traces
   - Check for common issues: dependency problems, permission errors, timeout issues, resource constraints
   - Examine the workflow configuration and environment setup

4. **Provide debugging guidance**:
   - Explain what went wrong in simple terms
   - Suggest specific fixes or configuration changes
   - Provide commands or code snippets to resolve the issue
   - Recommend preventive measures to avoid similar failures

5. **Context-aware solutions**:
   - Consider the project type (Python, Node.js, etc.) and suggest appropriate fixes
   - Check for recent changes that might have caused the failure
   - Suggest workflow improvements or optimizations

6. **Follow-up actions**:
   - Recommend next steps for testing the fix
   - Suggest monitoring or alerting improvements
   - Provide guidance on preventing similar issues

Workflow URL: {workflow_url}

Focus on providing actionable, specific solutions rather than generic troubleshooting advice. Use the GitHub CLI commands to gather comprehensive information about the failure.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice how prescriptive this prompt is. Rather than being a generic troubleshooting guide, it's a step-by-step guide that the agent can follow, down to the level of exact CLI commands to run. Critically, those CLI commands (&lt;code&gt;gh run list&lt;/code&gt;, &lt;code&gt;gh run view&lt;/code&gt;) are commands that I have auto-approved in my IDE, so the agent can execute the entire workflow autonomously without interrupting me for approval at each step.&lt;/p&gt;
&lt;p&gt;The prompt was written with AI assistance, which allows me to iterate to the level of detail I want with minimal effort. I start with a rough outline, then ask the agent to make it more specific, add command examples, and refine the steps until it's actionable enough for autonomous execution.&lt;/p&gt;
&lt;h2 id="use-plan-mode-for-complex-tasks"&gt;Use plan mode for complex tasks&lt;/h2&gt;&lt;p&gt;Plan mode in Cursor and Claude significantly improves agent performance on complex tasks. Users of AI-assisted coding tools consistently report that plan mode helps agents stay on course, compared to agents working without a structured plan. This mirrors how humans perform better with explicit plans.&lt;/p&gt;
&lt;p&gt;The mechanism is straightforward: the agent first generates a detailed plan, you review and refine it, then the agent executes against that plan. This separation of planning and execution prevents the agent from going down rabbit holes or making premature implementation decisions.&lt;/p&gt;
&lt;p&gt;In my experience, agents often complete tasks in one attempt after a few iterations on a well-defined plan. The key is ensuring the plan is specific and properly scoped before execution begins. I've found that plans work best when they include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Specific files and functions to modify&lt;/li&gt;
&lt;li&gt;Clear acceptance criteria&lt;/li&gt;
&lt;li&gt;Dependencies and ordering constraints&lt;/li&gt;
&lt;li&gt;Test cases or validation steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Without this structure, agents tend to make assumptions, skip steps, or get distracted by tangential improvements.&lt;/p&gt;
&lt;h2 id="managing-multiple-background-agents"&gt;Managing multiple background agents&lt;/h2&gt;&lt;p&gt;Multiple background agents can be powerful, but they require careful management. Unless agents are handling mundane, well-defined tasks, context switching between multiple active agents becomes challenging. At that point, you're operating at the speed of thought, which requires significant cognitive overhead.&lt;/p&gt;
&lt;p&gt;I've found that multiple agents work well when they're working on independent, well-scoped tasks. For example, one agent might be researching documentation while another refactors a specific module. But when tasks have dependencies or require coordination, a single agent with a clear plan tends to perform better than multiple agents trying to coordinate.&lt;/p&gt;
&lt;p&gt;The cognitive load turns out to be more than keeping track of what each agent is doing; we also need to ensure they don't conflict with each other. Two agents modifying the same file simultaneously, or one agent's changes breaking assumptions another agent made, creates more problems than it solves.&lt;/p&gt;
&lt;h2 id="additional-resources"&gt;Additional resources&lt;/h2&gt;&lt;p&gt;Others have written extensively about effective coding agent workflows. Here's a curated collection of resources I've found valuable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2025/Oct/25/coding-agent-tips/"&gt;Simon Willison's coding agent tips&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.geoffreylitt.com/2025/10/24/code-like-a-surgeon"&gt;Geoffrey Litt suggests coding like a surgeon&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/omarsar0/status/1984641893519839271"&gt;&lt;code&gt;@omarsar0&lt;/code&gt; on Twitter loves plan mode on Claude Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/mattpocockuk"&gt;&lt;code&gt;@mattpocockuk&lt;/code&gt; has awesome tips on how to use AI for coding&lt;/a&gt;, including &lt;a href="https://x.com/mattpocockuk/status/1985056806893211915"&gt;this tip&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://simonwillison.net/2025/Nov/6/async-code-research/"&gt;Simon Willison (again!) on async code research with coding agents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.linkedin.com/posts/sebastian-wallkoetter_my-favourite-question-to-spot-a-vibe-coder-activity-7394726959349592064--baU"&gt;Sebastian Wallkötter on preventing AI spaghetti through intermediate reviews&lt;/a&gt;: The key insight is that AI coding's bottleneck is code review, not code generation. Small mistakes compound when AI re-ingests its own errors as context. The solution: implement features in small increments with intermediate reviews, fixing "5 second issues" as you go, rather than letting mistakes accumulate into spaghetti code that takes hours to untangle.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What are your tips for safe ways to let your coding agent work autonomously? And what did you like most about this post? Let me know in the comments below!&lt;/p&gt;
</content></entry><entry><title>Use coding agents to write Marimo notebooks</title><link href="https://ericmjl.github.io/blog/2025/10/28/use-coding-agents-to-write-marimo-notebooks/" rel="alternate"/><updated>2025-10-28T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:820f6ef9-cdc4-384d-a08e-890efd1130b9</id><content type="html">&lt;p&gt;If you're like me, you might find coding with AI assistants somewhat addictive. And if you're like me, you might also like to write code in Marimo notebooks, the modern alternative to Jupyter that offers better reproducibility and cleaner Python development.&lt;/p&gt;
&lt;p&gt;Turns out there's a way to put these two together for automated Python development and data science workflows, creating a powerful combination for rapid prototyping and iterative coding.&lt;/p&gt;
&lt;h2 id="marimo-s-watch-flag"&gt;Marimo's &lt;code&gt;--watch&lt;/code&gt; Flag&lt;/h2&gt;&lt;p&gt;A few months ago, at SciPy 2025, my friend &lt;a href="https://trevorma.nz/"&gt;Trevor Manz&lt;/a&gt; showed me a cool neat trick for writing Marimo notebooks. Apart from launching a Marimo notebook in &lt;a href="https://docs.marimo.io/guides/package_management/inlining_dependencies/"&gt;sandbox mode&lt;/a&gt;, you add a &lt;code&gt;--watch&lt;/code&gt; flag:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;my_notebook.py&lt;span class="w"&gt; &lt;/span&gt;--watch
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When edits are made to the source file &lt;code&gt;notebook.py&lt;/code&gt;, they will now be reflected in the browser as well. This was my reaction:&lt;/p&gt;
&lt;p&gt;&lt;img src="minion-what.webp" alt="minion-what.webp"&gt;&lt;/p&gt;
&lt;p&gt;If you ever meet Trevor in person, he can confirm that reaction of mine.&lt;/p&gt;
&lt;h2 id="ensure-code-quality-with-marimo-check"&gt;Ensure code quality with &lt;code&gt;marimo check&lt;/code&gt;&lt;/h2&gt;&lt;p&gt;So now, AI coding assistants can write your Marimo notebooks for you... but it's not always going to be correct first time, right? After all, the latest features of Marimo are not going to be part of the large language model training sets.&lt;/p&gt;
&lt;p&gt;Turns out, Marimo also ships with a &lt;code&gt;check&lt;/code&gt; command that you can ask coding agents to call on:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;check&lt;span class="w"&gt; &lt;/span&gt;my_notebook.py
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And that will print to stdout any issues that Marimo finds that break its execution model, such as variables that are repeated variables or invalid cells.&lt;/p&gt;
&lt;p&gt;You can instruct coding agents to always run &lt;code&gt;marimo check&lt;/code&gt; by adding the following prompt (or analogous) into &lt;code&gt;AGENTS.md&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;When editing Marimo notebooks, always run &lt;span class="sb"&gt;`uvx marimo check`&lt;/span&gt; on the file and fix all issues that you find.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This will virtually guarantee correctly-written, AI-generated notebooks. All that's left for us as users is to check the correctness of the analysis that was done.&lt;/p&gt;
&lt;h2 id="real-world-use"&gt;Real-world use&lt;/h2&gt;&lt;p&gt;Now, AI coding assistants (like Cursor, GitHub Copilot, or Claude Code) can write and edit large chunks of Marimo notebook cells for you, check what they wrote, and fix any syntactic issues that show up. And by checking that the cells are syntactically valid. Now you can speed-run those routine and yet highly mundane data manipulation code-writing activities while making yourself an espresso drink. This aligns perfectly with my philosophy on &lt;a href="../../../../2019/3/20/how-i-work/"&gt;optimizing for productivity in data science workflows&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I've used this mode to speed-run first versions of &lt;a href="../../../4/3/bayesian-superiority-estimation-with-r2d2-priors-a-practical-guide-for-protein-screening/"&gt;probabilistic models in PyMC&lt;/a&gt;, create explainer notebooks for hard concepts, make notebooks that process data, and many, many more things that you'd usually be able to do within a coding notebook system. The key thing that makes this work is feedback given (via the command line) that the coding agent can use for self-correction.&lt;/p&gt;
&lt;h2 id="advanced-functionality-using-mcp-and-built-in-ai-features"&gt;Advanced functionality using MCP and built-in AI features&lt;/h2&gt;&lt;p&gt;It doesn't stop there, though. There's a new &lt;code&gt;--mcp&lt;/code&gt; flag that makes a notebook an MCP server that coding agents can connect to; read more about it &lt;a href="https://opensourcedev.substack.com/p/beyond-chatbots-how-i-turned-python"&gt;here&lt;/a&gt;. Marimo also has built-in AI editing capabilities itself as well. Check out the functionality &lt;a href="https://docs.marimo.io/guides/editor_features/ai_completion/#custom-copilots"&gt;here&lt;/a&gt;, as well as Vincent Warmerdam's short video on &lt;a href="https://www.youtube.com/shorts/CnHOGE46x3o"&gt;using coding agents from &lt;em&gt;within&lt;/em&gt; Marimo&lt;/a&gt;. He's got my vote for best facial/eyebrow expressions from a coding YouTuber!&lt;/p&gt;
&lt;h2 id="addendum"&gt;Addendum&lt;/h2&gt;&lt;p&gt;After sharing this post on LinkedIn, Séverin H. shared a couple of additional use cases worth highlighting:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;One use case I would also recommend is getting the coding assistant to run queries for you, especially when it is to debug a existing query. You can ask [it] to check for corner cases (and especially dig into the data to understand the corner cases).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(&lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7391852642743775232?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7391852642743775232%2C7391858466161606656%29&amp;amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287391858466161606656%2Curn%3Ali%3Aactivity%3A7391852642743775232%29"&gt;LinkedIn comment&lt;/a&gt;)&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The &lt;code&gt;--watch&lt;/code&gt; flag is indeed very interesting use case. Also to note they created a Claude.md to get you started that you can directly curl:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;https://docs.marimo.io/CLAUDE.md&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;agents.md&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Some more reference from their blog: &lt;a href="https://marimo.io/blog/claude-code"&gt;https://marimo.io/blog/claude-code&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(&lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7391852642743775232?commentUrn=urn%3Ali%3Acomment%3A%28activity%3A7391852642743775232%2C7391854914886533121%29&amp;amp;dashCommentUrn=urn%3Ali%3Afsd_comment%3A%287391854914886533121%2Curn%3Ali%3Aactivity%3A7391852642743775232%29"&gt;LinkedIn comment&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;Thanks for the suggestions, Séverin!&lt;/p&gt;
</content></entry><entry><title>Exploring Skills vs MCP Servers</title><link href="https://ericmjl.github.io/blog/2025/10/20/exploring-skills-vs-mcp-servers/" rel="alternate"/><updated>2025-10-20T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:cb890a27-6c6a-351a-b47a-c3db04a3f25d</id><content type="html">&lt;p&gt;I spent time digging through Anthropic's skills repository. These are my first impressions, organized for clarity and future reference.&lt;/p&gt;
&lt;h2 id="what-the-anthropic-skills-repository-offers"&gt;What the Anthropic Skills repository offers&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Creative &amp;amp; design workflows&lt;/strong&gt;: &lt;code&gt;algorithmic-art&lt;/code&gt; (generative art with p5.js), &lt;code&gt;canvas-design&lt;/code&gt; (beautiful PNG/PDF outputs guided by design philosophies), &lt;code&gt;theme-factory&lt;/code&gt; (pre-set or on-the-fly themes), and &lt;code&gt;slack-gif-creator&lt;/code&gt; (animated GIFs tuned for Slack). These are turnkey “taste plus tooling” bundles that let the model produce high-quality visuals with consistent aesthetics.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document skills for real formats&lt;/strong&gt;: &lt;code&gt;document-skills/&lt;/code&gt; cover &lt;code&gt;pptx&lt;/code&gt;, &lt;code&gt;docx&lt;/code&gt;, &lt;code&gt;pdf&lt;/code&gt;, and &lt;code&gt;xlsx&lt;/code&gt; with serious capabilities: layout/templates, tracked changes and comments, text/table extraction, merges/splits, charting, formulas, and formatting preservation. This feels like a pragmatic spec+runtime for working with binary formats—lean instructions up front, heavy lifting when needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Development &amp;amp; technical utilities&lt;/strong&gt;: &lt;code&gt;artifacts-builder&lt;/code&gt; (compose complex Claude HTML artifacts using React/Tailwind/shadcn), &lt;code&gt;webapp-testing&lt;/code&gt; (Playwright-driven UI testing), and &lt;code&gt;mcp-builder&lt;/code&gt; (guidance for creating high-quality MCP servers). These reduce boilerplate for the “build and test” loop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Enterprise &amp;amp; communication&lt;/strong&gt;: &lt;code&gt;brand-guidelines&lt;/code&gt; (apply Anthropic’s official brand colors and typography) and &lt;code&gt;internal-comms&lt;/code&gt; (status reports, newsletters, FAQs). These encode editorial and brand guardrails so outputs stay on-message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Meta skills and templates&lt;/strong&gt;: &lt;code&gt;skill-creator&lt;/code&gt; and &lt;code&gt;template-skill&lt;/code&gt; show how to structure your own skills: a folder per skill with a &lt;code&gt;SKILL.md&lt;/code&gt; (YAML front matter for &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, plus instructions/examples/guidelines), optional scripts, and assets. This is the pattern to replicate.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you want the source for these examples, it’s viewable in the repo. Start here: &lt;code&gt;https://github.com/anthropics/skills&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="how-skills-are-loaded-and-used"&gt;How skills are loaded and used&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Minimal prompt footprint&lt;/strong&gt;: A skill's short description is passed up front. The larger &lt;code&gt;skill.md&lt;/code&gt; is only read when the model decides it needs more detail.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;On-demand details&lt;/strong&gt;: The model can iterate (ReAct loop) to fetch instructions and then execute scripts or read additional files.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This access pattern keeps the initial token budget small and defers detail until it’s actually needed.&lt;/p&gt;
&lt;h2 id="contrast-with-mcp-servers"&gt;Contrast with MCP servers&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MCP call shape&lt;/strong&gt;: Tool names and descriptions are typically sent on every call. That keeps tools globally discoverable but increases token overhead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Skills call shape&lt;/strong&gt;: A tiny descriptor up front; details fetched lazily. Lower baseline token cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Distribution model&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;MCP: Centrally hostable (e.g. web server) or vendable (e.g., a Python package). Easy to version, release, and update for many users at once.&lt;/li&gt;
&lt;li&gt;Skills: Feel local-first. You can drag-and-drop into a Claude workspace. Easy to customize, but harder to standardize and propagate updates across a team.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Given current industry patterns, MCP servers are the widely accepted way to expose functionality to LLMs across tools and vendors. Skills are Anthropic-specific at the moment.&lt;/p&gt;
&lt;h2 id="token-efficiency-and-why-its-emphasized"&gt;Token efficiency (and why it’s emphasized)&lt;/h2&gt;&lt;p&gt;Anthropic’s materials lean into token efficiency. The cost of LLM calls adds up, and repeatedly sending long tool descriptions can be expensive. Skills reduce baseline tokens: spend a handful of tokens to register intent, read detail only when needed, then execute. That’s the economic story.&lt;/p&gt;
&lt;h2 id="practical-trade-offs"&gt;Practical trade-offs&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Standardization vs customization&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;MCP servers: Strong for shared, versioned, and centrally updated capabilities.&lt;/li&gt;
&lt;li&gt;Skills: Great for rapid, local customization without infrastructure.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discovery vs cost&lt;/strong&gt;:&lt;ul&gt;
&lt;li&gt;MCP: High discoverability; the model always sees the tools. Higher token floor.&lt;/li&gt;
&lt;li&gt;Skills: Low token floor; details fetched when needed. Requires the model to choose to read more.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="open-questions-im-tracking"&gt;Open questions I’m tracking&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;How will teams distribute and update skills at scale without a central registry or packaging story?&lt;/li&gt;
&lt;li&gt;Will skills gain cross-vendor support, or remain Anthropic-only?&lt;/li&gt;
&lt;li&gt;What’s the best practice to map a complex skill into smaller, composable units without losing clarity?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="early-take"&gt;Early take&lt;/h2&gt;&lt;p&gt;IMO, skills are a clear attempt to lower token costs and streamline task-specific workflows with minimal upfront context. MCP servers remain the well-understood, cross-ecosystem pattern for exposing capabilities. If your goal is a shareable, versioned interface for many users, MCP is still the safer default. If you need quick, local customization inside Claude with a lean prompt footprint, skills are compelling. But this field has been evolving at breawkneck speed anyways, so expect changes.&lt;/p&gt;
</content></entry><entry><title>How to expose any documentation to any LLM agent</title><link href="https://ericmjl.github.io/blog/2025/10/19/how-to-expose-any-documentation-to-any-llm-agent/" rel="alternate"/><updated>2025-10-19T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:70188213-fcc1-328b-8c1f-f1a5fa7f43e1</id><content type="html">&lt;p&gt;Like cars that lose value as soon as they roll off the lot, LLMs become outdated as soon as their training sets are fixed. Software documentation evolves constantly—new features, API changes, bug fixes, and best practices emerge daily. Yet AI agents are stuck with whatever knowledge was captured in their training data, creating a fundamental mismatch between what they know and what developers actually need in real-time.&lt;/p&gt;
&lt;p&gt;Building LlamaBot taught me something unexpected: the hardest part of AI-assisted development isn't writing better prompts or designing cleaner abstractions. It's equipping AI agents with up-to-date information in a stable, standardized fashion.&lt;/p&gt;
&lt;p&gt;Most developers know the frustration of context-switching between code and documentation. You're deep in a coding session, need to check how a specific function works, and suddenly you're hunting through static documentation files. AI agents face this same problem, but with an added layer of complexity—they need structured, queryable access to documentation that can be searched semantically.&lt;/p&gt;
&lt;p&gt;I discovered that web searches by coding agents were less reliable than manually adding context, but manual approaches don't scale. The solution emerged through the Model Context Protocol (MCP), a standard that enables LLMs to interact with external tools and data sources. In LlamaBot v0.13.10, I introduced a documentation MCP server that automatically equips AI agents with current information. This enables AI agents to access organizational knowledge, process documentation, and domain expertise in structured ways.&lt;/p&gt;
&lt;h2 id="the-obsolescence-problem-in-ai-assisted-development"&gt;The obsolescence problem in AI-assisted development&lt;/h2&gt;&lt;p&gt;The core issue more than mere documentation access, it's about obsolescence. LLMs are trained on data that becomes outdated the moment it's fixed in their training sets. Meanwhile, software documentation evolves constantly. New features are added, APIs change, bugs are fixed, and best practices emerge. Yet AI agents remain frozen in time, working with knowledge that may be months or years out of date.&lt;/p&gt;
&lt;p&gt;Consider a typical data science workflow: you're building an AI pipeline and need to understand how LlamaBot's StructuredBot handles data validation. The AI agent might reference documentation from six months ago, missing critical updates or new features that could solve your problem more elegantly. This creates a fundamental mismatch between what the agent knows and what's actually available.&lt;/p&gt;
&lt;p&gt;The deeper problem is that AI agents need structured, queryable access to documentation that can be searched semantically and updated automatically. They need to understand not just what functions exist, but how they relate to each other, what patterns they follow, and how they fit into broader workflows. Static documentation simply cannot provide this level of contextual understanding, particularly in data science environments where teams maintain scattered knowledge across wikis, Slack threads, and onboarding documents.&lt;/p&gt;
&lt;h2 id="building-a-semantic-documentation-layer"&gt;Building a semantic documentation layer&lt;/h2&gt;&lt;p&gt;LlamaBot's MCP server demonstrates how to give AI agents structured access to its documentation by creating a dynamic, queryable knowledge base that agents can search semantically. The &lt;a href="https://github.com/ericmjl/llamabot/blob/main/llamabot/mcp_server.py"&gt;implementation&lt;/a&gt; centers around a single tool:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nd"&gt;@mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;docs_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Search through LlamaBot documentation and source code.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docstore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;results&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This interface sits in front of a data pipeline that builds a vector database for the documentation. The server fetches the latest documentation from GitHub, extracts Python module docstrings from source code, and constructs a LanceDB vector database optimized for semantic search. The database is built during CI/CD and packaged directly with the wheel distribution, giving users instant access without setup while staying current with each release.&lt;/p&gt;
&lt;p&gt;This approach works with any AI agent system through the MCP protocol, providing a standardized way to keep AI agents current with documentation.&lt;/p&gt;
&lt;h2 id="the-architecture-behind-semantic-documentation"&gt;The architecture behind semantic documentation&lt;/h2&gt;&lt;p&gt;The MCP server combines several technologies to create a robust documentation system. FastMCP handles the protocol implementation, enabling seamless communication between AI agents and the documentation database. LanceDB powers the semantic search capabilities, leveraging LlamaBot's existing &lt;code&gt;LanceDBDocStore&lt;/code&gt; class with hybrid search and reranking for optimal results.&lt;/p&gt;
&lt;p&gt;The system uses the checked-out documentation from the repository during the CI/CD build process, ensuring the packaged database contains current information. The build script first attempts to fetch docs from GitHub, but falls back to the local &lt;code&gt;docs/&lt;/code&gt; directory when available, making it work seamlessly in both CI/CD and development environments. The build process runs the &lt;code&gt;scripts/build_mcp_docs.py&lt;/code&gt; script during CI/CD, which creates the LanceDB database and copies it to &lt;code&gt;llamabot/data/mcp_docs/&lt;/code&gt; for packaging.&lt;/p&gt;
&lt;p&gt;I believe this architecture represents a fundamental shift in how we think about documentation for AI systems. Instead of treating documentation as static reference material, we're creating dynamic, queryable knowledge bases that AI agents can interact with directly.&lt;/p&gt;
&lt;h2 id="the-core-pattern-to-replicate"&gt;The core pattern to replicate&lt;/h2&gt;&lt;p&gt;The LlamaBot MCP server follows a straightforward pattern that any package or documentation source can replicate. Here's the essential blueprint:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Build a semantic database during CI/CD&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extract documentation from your source (GitHub, local docs, API references)&lt;/li&gt;
&lt;li&gt;Parse and chunk the content appropriately for your domain&lt;/li&gt;
&lt;li&gt;Create a vector database (LanceDB, Chroma, or similar) with semantic search capabilities&lt;/li&gt;
&lt;li&gt;Package the database with your distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;2. Create an MCP server with a search tool&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nd"&gt;@mcp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;docs_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Search through your documentation and source code.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;docstore&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_results&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;query&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;results&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3. Make it discoverable and configurable&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide a simple launch command (like &lt;code&gt;yourpackage mcp launch&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Include clear setup instructions for MCP-compatible tools&lt;/li&gt;
&lt;li&gt;Ensure the database updates automatically with each release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;4. Design for your specific knowledge domain&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Include not just API docs, but process documentation, examples, and institutional knowledge&lt;/li&gt;
&lt;li&gt;Structure the content for semantic search rather than keyword matching&lt;/li&gt;
&lt;li&gt;Consider what context your users need most when working with AI agents&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="seamless-integration-with-modern-development-tools"&gt;Seamless integration with modern development tools&lt;/h2&gt;&lt;p&gt;The MCP server works with any MCP-compatible coding environment, including Cursor, VSCode, and other modern development tools. Configuration requires a single command in your MCP settings:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;--with&lt;span class="w"&gt; &lt;/span&gt;llamabot&lt;span class="o"&gt;[&lt;/span&gt;all&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;llamabot&lt;span class="w"&gt; &lt;/span&gt;mcp&lt;span class="w"&gt; &lt;/span&gt;launch
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once configured, AI agents can query LlamaBot documentation using natural language queries. Ask "How do I use StructuredBot for data extraction?" and the agent receives structured results with content, relevance scores, and metadata. This contextual information enables agents to provide accurate, up-to-date assistance without manual documentation lookup.&lt;/p&gt;
&lt;p&gt;This reduces context-switching between code and documentation. AI agents can access relevant information and provide suggestions based on current code patterns and usage examples. This approach is particularly valuable for data science teams who need to maintain consistency across experiments while leveraging the latest library capabilities.&lt;/p&gt;
&lt;h2 id="comparing-documentation-approaches-for-ai-agents"&gt;Comparing documentation approaches for AI agents&lt;/h2&gt;&lt;p&gt;There are several ways to provide documentation to AI agents, each with distinct trade-offs. Understanding these approaches helps clarify why the MCP server approach represents a significant improvement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Native tool documentation&lt;/strong&gt; (like Cursor's built-in docs capabilities) offers seamless integration and can fetch docs from online sources, but you're limited to how the tool fetches those docs. It may not be able to access certain systems gated behind access controls or include custom organizational knowledge and process documentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Manual repository inclusion&lt;/strong&gt; works well for users familiar with IDEs, workspaces, and development concepts, but it requires familiarity with these practices that are unfamiliar to non-technical users or individual developers. It also doesn't scale beyond individual developers. The documentation becomes part of the context window, consuming valuable tokens and potentially overwhelming the agent with irrelevant information.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Copy-paste or file upload&lt;/strong&gt; (like Claude Projects) provides flexibility for non-technical users but creates maintenance overhead. You must manually update documentation when it changes, and there's no semantic search capability—agents can only work with what you explicitly provide.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Web search by agents&lt;/strong&gt; seems convenient but creates inefficiency in the development workflow. If the documentation is up-to-date, the LLM will find it eventually, but it requires multiple iterations of web searches to locate the right information. I discovered this firsthand when building LlamaBot—web searches by coding agents required more iterations than directly providing context, but manual approaches don't scale.&lt;/p&gt;
&lt;p&gt;The MCP server approach provides automatic updates, semantic search, system-agnostic compatibility, and organizational knowledge integration. It offers a standardized way to keep AI agents current with evolving documentation. The trade-off is initial setup complexity, but this is mitigated by the pre-built databases that ship with packages.&lt;/p&gt;
&lt;h2 id="beyond-software-documentation-surfacing-any-process-knowledge"&gt;Beyond software documentation - surfacing any process knowledge&lt;/h2&gt;&lt;p&gt;The MCP approach extends beyond software documentation. The LlamaBot server gives an example of how organizations can surface their process documentation, institutional knowledge, and domain expertise.&lt;/p&gt;
&lt;p&gt;I believe that data science teams could transform their workflow documentation—experimental protocols, data validation procedures, model evaluation criteria, or deployment checklists—from scattered wikis, Slack threads, and buried onboarding documents into structured, queryable knowledge bases that AI agents can access and reference during development.&lt;/p&gt;
&lt;p&gt;I can see this approach scaling beyond individual libraries to entire organizational knowledge. We have all imagined AI agents that can query your team's coding standards, understand your deployment procedures, or reference your data governance policies—all without leaving their development environment. Each organization would maintain their own specialized knowledge base, creating networks of interconnected AI-accessible process documentation. How would one implement this? A documentation MCP server may be a great way to start.&lt;/p&gt;
&lt;p&gt;This approach isn't just for software docs. Imagine surfacing your team's process knowledge, onboarding guides, or even those golden nuggets buried in Slack threads. The MCP server pattern can turn scattered, informal knowledge into a living, searchable resource for both humans and AI agents, especially if you treat your processes as versioned software to be exposed to AI agents!&lt;/p&gt;
&lt;p&gt;In my experience, the most valuable knowledge in organizations often exists in informal channels—Slack conversations, email threads, or tribal knowledge that never gets documented. I believe the MCP approach provides a framework for capturing and surfacing this knowledge in ways that AI agents can understand and reference.&lt;/p&gt;
&lt;h2 id="the-future-of-ai-assisted-development"&gt;The future of AI-assisted development&lt;/h2&gt;&lt;p&gt;Future iterations could include real-time updates that rebuild databases when documentation changes, cross-organizational knowledge graphs, and usage pattern analysis that learns from how teams implement processes.&lt;/p&gt;
&lt;p&gt;The goal is to make AI agents active participants in organizational processes, capable of understanding team workflows and providing context-aware recommendations.&lt;/p&gt;
&lt;p&gt;This vision requires rethinking how we structure and maintain organizational knowledge. Instead of writing documentation solely for human consumption, we need to design knowledge systems that serve both human team members and AI agents, creating a symbiotic relationship between human creativity and AI capability while preserving institutional knowledge in accessible, queryable formats.&lt;/p&gt;
&lt;h2 id="getting-started-with-semantic-documentation"&gt;Getting started with semantic documentation&lt;/h2&gt;&lt;p&gt;The MCP server is available in LlamaBot v0.13.10 and later. Getting started requires minimal setup:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Install LlamaBot with MCP support: &lt;code&gt;pip install llamabot[all]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Configure your coding tool to use the MCP server&lt;/li&gt;
&lt;li&gt;Begin coding with AI agents that understand LlamaBot's capabilities&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The documentation database ships pre-built with the package, eliminating setup friction. The server exposes a single &lt;code&gt;docs_search&lt;/code&gt; tool that agents can use to find relevant documentation and source code information, creating a seamless development experience.&lt;/p&gt;
&lt;p&gt;This approach makes documentation an integral part of the AI agent's toolkit, resulting in more capable assistants that can help developers work more effectively.&lt;/p&gt;
&lt;p&gt;The future of AI-assisted development involves better integration between AI agents and the tools they need. LlamaBot's MCP server demonstrates how this integration can work in practice.&lt;/p&gt;
</content></entry><entry><title>A practical comparison of DSPy and LlamaBot for structured LLM applications</title><link href="https://ericmjl.github.io/blog/2025/10/18/a-practical-comparison-of-dspy-and-llamabot-for-structured-llm-applications/" rel="alternate"/><updated>2025-10-18T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0282121b-3e33-3ecb-9837-afd6b1121706</id><content type="html">&lt;p&gt;When Omar Khattabe presented &lt;a href="https://dspy.ai"&gt;DSPy 3.0&lt;/a&gt; at PyData Boston Cambridge last week, I finally had the chance to dig into a framework that's been generating significant buzz in the LLM development community. As someone who's built structured LLM applications with &lt;a href="https://ericmjl.github.io/llamabot/"&gt;LlamaBot&lt;/a&gt;, I was particularly curious about DSPy's core claim: that signatures represent the only abstraction you need for LLM-powered programs.&lt;/p&gt;
&lt;p&gt;The presentation focused on two key concepts: signatures as a new LLM abstraction and prompt optimization techniques. But what caught my attention was the practical similarity between DSPy's approach and what I've been doing with LlamaBot's StructuredBot. This led me to build a direct comparison using a real-world example from my personal expense tracking application.&lt;/p&gt;
&lt;h2 id="the-structured-llm-challenge"&gt;The structured LLM challenge&lt;/h2&gt;&lt;p&gt;Most developers working with LLMs face the same fundamental problem: how do you reliably extract structured data from unstructured inputs? Whether you're processing receipts, parsing documents, or analyzing text, you need consistent, typed outputs that integrate cleanly with your existing systems.&lt;/p&gt;
&lt;p&gt;Traditional approaches rely heavily on natural language prompts, which are fragile, hard to maintain, and difficult to optimize. DSPy proposes a different path through its signature abstraction, claiming this eliminates the need for verbose prompt engineering.&lt;/p&gt;
&lt;h2 id="a-real-world-comparison-receipt-processing"&gt;A real-world comparison: Receipt processing&lt;/h2&gt;&lt;p&gt;To test DSPy's claims, I built a practical comparison using an expense extraction system I developed for personal use. This application processes receipts in various formats (PNG, PDF, JPG, WEBP) and automatically extracts structured expense data into Notion — essentially a lightweight alternative to enterprise expense management systems.&lt;/p&gt;
&lt;p&gt;The challenge here is typical of structured LLM applications: converting unstructured visual and textual data into consistent, typed outputs that integrate with existing workflows. Let's see how both frameworks handle this task.&lt;/p&gt;
&lt;h3 id="llamabot-s-structuredbot-approach"&gt;LlamaBot's StructuredBot approach&lt;/h3&gt;&lt;p&gt;LlamaBot uses Pydantic models to define structured outputs, leveraging Python's type system for validation and documentation. The approach emphasizes explicit data modeling with detailed field descriptions:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pydantic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;enum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pathlib&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;llamabot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;lmb&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FlowType&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;MONEY_OUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Money Out&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;MONEY_IN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Money In&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;TypeEnum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;PAYMENT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Payment&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;INVOICE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Invoice&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;PaymentMethodEnum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;CASH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Cash&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;BANK_TRANSFER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Bank Transfer&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;CREDIT_CARD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Credit Card&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;CHECK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Check&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExpenseData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;transaction_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Short, memorable description of the purchase. E.g.: &amp;#39;Anker Dock&amp;#39;, &amp;#39;Coffee at Triangle Bar&amp;#39;, &amp;#39;dbrand laptop skin&amp;#39;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;transaction date&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;transaction amount&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Business category, e.g. Office Supplies, Travel, Meals&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TypeEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Either Payment or Invoice&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FlowType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Either &amp;#39;Money Out&amp;#39; or &amp;#39;Money In&amp;#39;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payment_method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PaymentMethodEnum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;How the payment was made.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;purpose&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Brief business purpose or description of the expense.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reference_number&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Invoice/receipt number if visible&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;Person responsible or who made the purchase if mentioned.&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage&lt;/span&gt;
&lt;span class="n"&gt;bot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lmb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StructuredBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;pydantic_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ExpenseData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/gemma3n:latest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/path/to/receipt.png&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="dspy-s-signature-approach"&gt;DSPy's signature approach&lt;/h3&gt;&lt;p&gt;DSPy takes a different approach with its signature abstraction, which defines both inputs and outputs in a single class. The framework emphasizes simplicity and automatic prompt optimization:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dspy&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ExpenseExtraction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Signature&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Extract expense information from receipt images.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

    &lt;span class="n"&gt;receipt_image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Image&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Receipt image&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;transaction_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Short description of the purchase&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transaction date (YYYY-MM-DD)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Total transaction amount (number, no currency symbols)&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;category&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Business category (e.g., Office Supplies, Travel, Meals)&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Transaction type, either &amp;#39;Payment&amp;#39; or &amp;#39;Invoice&amp;#39;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;flow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Cash flow direction, either &amp;#39;Money Out&amp;#39; or &amp;#39;Money In&amp;#39;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;payment_method&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;How the payment was made (e.g., Cash, Bank Transfer, Credit Card, Check)&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;purpose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Brief business purpose or description&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;reference_number&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Invoice/receipt number if present&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;person&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OutputField&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Person involved, if mentioned&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Usage&lt;/span&gt;
&lt;span class="n"&gt;lm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;ollama_chat/gemma3n:latest&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;lm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dspy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ExpenseExtraction&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt_image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="comparing-the-approaches"&gt;Comparing the approaches&lt;/h2&gt;&lt;p&gt;Both frameworks successfully extracted structured data from receipt images, but they take fundamentally different approaches to the problem.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LlamaBot's StructuredBot&lt;/strong&gt; leverages Python's existing type system through Pydantic models. This approach provides several advantages: automatic validation, IDE support, and integration with existing Python data processing pipelines. The explicit type definitions make the data contract clear and enforceable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DSPy's signatures&lt;/strong&gt; offer a more streamlined interface that combines input and output definitions in a single class. The framework's strength lies in its automatic prompt optimization capabilities, which can improve performance over time without manual intervention.&lt;/p&gt;
&lt;h2 id="key-differences-in-practice"&gt;Key differences in practice&lt;/h2&gt;&lt;p&gt;The most noticeable difference is verbosity. LlamaBot requires more explicit type definitions and imports, while DSPy's signature approach is more concise. However, this conciseness may come at the cost of some type safety and IDE support that Pydantic provides.&lt;/p&gt;
&lt;p&gt;Both frameworks use LiteLLM for model routing, making it easy to switch between different LLM providers. The model configuration syntax is identical, which suggests a common underlying architecture.&lt;/p&gt;
&lt;h2 id="the-schema-first-principle"&gt;The schema-first principle&lt;/h2&gt;&lt;p&gt;Regardless of which framework you choose, structured LLM applications require careful upfront schema design. The bulk of development time goes into defining your data model, not writing prompts. This schema-first approach is what makes these frameworks powerful—they force you to think clearly about your data requirements before implementation.&lt;/p&gt;
&lt;h2 id="looking-ahead-dspy-s-broader-vision"&gt;Looking ahead: DSPy's broader vision&lt;/h2&gt;&lt;p&gt;DSPy's claim that signatures are the only abstraction needed for LLM applications is ambitious but not entirely accurate. The framework includes additional abstractions like modules and optimizers that handle more complex scenarios. Signatures represent the core abstraction for simple input-output transformations, but building production LLM applications often requires more sophisticated orchestration.&lt;/p&gt;
&lt;p&gt;I'm planning to explore DSPy's more advanced features as I rebuild LlamaBot's agent abstractions. The goal is to understand how to construct autonomous LLM agent frameworks rather than individual agents—a challenge that requires thinking beyond simple input-output mappings.&lt;/p&gt;
&lt;p&gt;Being unfamiliar with DSPy's documentation initially, I found it challenging to follow, but thanks to fellow PyData Boston Cambridge organizer &lt;a href="https://www.linkedin.com/in/nnssa/"&gt;Nash Sabti&lt;/a&gt;'s guidance, I was able to make it happen and build this comparison.&lt;/p&gt;
&lt;p&gt;The structured LLM landscape is rapidly evolving, and frameworks like DSPy and LlamaBot are pushing the boundaries of what's possible. The key insight is that successful LLM applications require the same engineering discipline as traditional software: clear interfaces, robust error handling, and maintainable abstractions.&lt;/p&gt;
</content></entry><entry><title>How to Use Coding Agents Effectively</title><link href="https://ericmjl.github.io/blog/2025/10/14/how-to-use-coding-agents-effectively/" rel="alternate"/><updated>2025-10-14T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:11cedff3-fc21-31fb-b882-ef11b228c2e1</id><content type="html">&lt;p&gt;This past week, I went on a building spree, a part of my ongoing ultralearning practice, and built multiple projects using AI coding assistants. After many months of working with AI coding assistants on real projects, I've learned that effective agent usage requires more than just good prompts. You need systematic workflows, external memory systems, and a willingness to let the agent fail fast so you can discover architectural boundaries.&lt;/p&gt;
&lt;p&gt;These are the patterns that make coding agents productive.&lt;/p&gt;
&lt;h2 id="starting-out"&gt;Starting Out&lt;/h2&gt;&lt;p&gt;Effective agent usage starts with establishing a disciplined workflow that covers the complete development lifecycle. This isn't just about fancy prompts; we're talking about creating a repeatable process that works from start to finish.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Complete Lifecycle&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="mermaid"&gt;
flowchart TD
    A[Plan] --&gt; B[Write Tests]
    B --&gt; C[Implement Code]
    C --&gt; D[Run Tests]
    D --&gt; E{Tests Pass?}
    E --&gt;|No| F[Fix Issues]
    F --&gt; D
    E --&gt;|Yes| G[Document]
    G --&gt; H[Run Full Test Suite]
    H --&gt; I{All Tests Pass?}
    I --&gt;|No| F
    I --&gt;|Yes| J[Complete]

    style A fill:#e1f5fe
    style B fill:#f3e5f5
    style C fill:#e8f5e8
    style D fill:#fff3e0
    style G fill:#f1f8e9
    style J fill:#e8f5e8
&lt;/pre&gt;&lt;p&gt;Here's the systematic workflow that works best with coding agents:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plan first, then execute. Break your work into planning and execution phases, just like you would if writing code yourself. Have the agent write planning documents it can follow. This separation matters because planning and execution often use different parts of the model, and sometimes "dumber" models execute plans better than expensive ones.&lt;/li&gt;
&lt;li&gt;Write tests before implementation. This is where TDD becomes crucial with agents. Write tests first, then implement, then test the code. When tests pass, document. This workflow becomes more important with AI assistants because they're working with small context windows compared to your entire codebase. You must have the AI write tests for everything it generates.&lt;/li&gt;
&lt;li&gt;Implement with clear feedback loops. The proper TDD flow is: tests are always written first, executed and failed (because the implementation is lacking), then implemented, and executed again—ideally succeeding on the first try. This is super important for highest effectiveness with coding agents. The AI needs the clear feedback loop of failing tests to understand what to implement.&lt;/li&gt;
&lt;li&gt;Document as you go. When tests pass, document the implementation. This creates a complete record of what was built and why.&lt;/li&gt;
&lt;li&gt;Loop back to tests until everything is fixed. This is the critical step that many people miss. Don't stop at the first passing test—run the full test suite, check edge cases, and iterate until all tests pass consistently. The agent should keep running tests and fixing issues until the entire system is stable.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Learn your tool's shortcuts and modes. In Cursor, for example, you can open a new agent window with Cmd+E, and use Shift+Tab to toggle to plan mode (yellow colored). These modes work different parts of the model—planning models are better at analyzing code and planning than executing, while execution models are cheaper and sometimes more reliable at following plans.&lt;/p&gt;
&lt;p&gt;In VS Code with GitHub Copilot, you can define custom modes. You can even get Agent Mode to write a Planning Mode for you as a way to bootstrap Plan Mode. This gives you specialized interfaces for different types of work.&lt;/p&gt;
&lt;p&gt;Most of us software builders like to do the building part, not the verification part. TDD with agents lets you delegate the tedious verification work while keeping the fun building part for humans, as long as you review the tests the agent writes. This is another place where agents excel at taking over work we'd rather not do ourselves.&lt;/p&gt;
&lt;p&gt;Without this discipline, you'll find yourself debugging issues that could have been caught earlier. The complete lifecycle ensures that every piece of code is tested, documented, and verified before moving on.&lt;/p&gt;
&lt;p&gt;Finally, break work into chunks you can maintain concentration for during review. This takes practice getting used to an LLM's outputs, but it's important for effectiveness. Start with smaller scopes and gradually increase as you get comfortable with the agent's output patterns. The goal is to find the sweet spot where you can maintain focus while the agent does meaningful work.&lt;/p&gt;
&lt;h2 id="building-momentum"&gt;Building Momentum&lt;/h2&gt;&lt;p&gt;When starting a new project, don't try to get everything right the first time. Instead, speed-run your project twice, perhaps even thrice, in quick iteration mode. Just accept and vibe-code your way to the point where it gets hard for the LLM to do what you're asking.&lt;/p&gt;
&lt;p&gt;On each speed-run, you'll likely find yourself cornered architecturally. Step back and diagnose what's going wrong. Then speed-run the process once more to see if you can corner yourself another way. On your third try, you'll have made enough mistakes to clarify the mental model of the problem.&lt;/p&gt;
&lt;p&gt;I recently built a dataset versioning package called Kirin this way. It took three iterations over about a week to get the architecture right. The first two attempts helped me understand the problem space; the third attempt succeeded because I had learned the boundaries. The UI was done twice, and only on the third try did I get it right—all within about a week. This really helps with the design process, similar to the principles in "Design of Everyday Things."&lt;/p&gt;
&lt;h2 id="systematic-improvement"&gt;Systematic Improvement&lt;/h2&gt;&lt;p&gt;Once you have a working system, agents work well for systematic improvement tasks. The key is to ask them to prioritize rather than trying to fix everything at once.&lt;/p&gt;
&lt;p&gt;Test coverage improvement: Instead of asking the agent to improve coverage on every line, ask it to prioritize based on highest impact for fewest changes. Get its ranking of issues, then pick the one you understand and can review. Sometimes you might find the 2nd or 3rd highest ranked issue to be the one you understand and can review, which is super important here. Then you pick, and build a plan around it before executing.&lt;/p&gt;
&lt;p&gt;Ask the agent to give you its ranking of issues with explanations. This helps you understand not just what to fix, but why it matters and what the trade-offs are.&lt;/p&gt;
&lt;p&gt;Refactoring: Look across a class of files (like HTML templates) and ask the agent to identify refactoring opportunities. Again, ask it to prioritize, pick one, and record the others as GitHub issues for later. Pick two more categories and record them as GitHub issues, and tackle them later.&lt;/p&gt;
&lt;p&gt;For example, ask the agent to look across HTML Jinja templates and identify places where common HTML elements can be reused. Use the same prioritization trick: ask it to rank opportunities, pick the one you understand, and build a plan around it.&lt;/p&gt;
&lt;p&gt;Documentation review: Have the agent examine all docs in your repo and identify where docs document something not present in code, where there are gaps, and where docs are inaccurate. Prioritize major categories, pick one to tackle, and leave the others as GitHub issues.&lt;/p&gt;
&lt;p&gt;Ask the agent to identify three specific problems: (a) where docs document something not present in code, (b) where there are gaps (things in code not documented), and (c) where docs are inaccurate relative to what's present in the code. Again, prioritize major categories, pick one to tackle, and leave the others as GitHub issues.&lt;/p&gt;
&lt;h2 id="advanced-patterns"&gt;Advanced Patterns&lt;/h2&gt;&lt;p&gt;Your repository's issue tracker becomes an organized external memory system. It's stateful, has conversation records, and is plain text in Markdown. Use it liberally.&lt;/p&gt;
&lt;p&gt;When you have plans you don't want to act on immediately, ask the agent to post them as GitHub issues using the &lt;code&gt;gh&lt;/code&gt; CLI. This prevents losing track of ideas and creates a backlog you can return to.&lt;/p&gt;
&lt;p&gt;Use this prompt:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"ok, I would like you to put this up on github as an issue. use the gh cli to do that. check that i'm logged in as ericmjl and not on any other account."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This ensures the issue gets created in the right repository with the right account.&lt;/p&gt;
&lt;p&gt;For existing issues, ask the agent to evaluate whether they're still relevant and give its reasons. Codebases evolve, and you might be able to deprecate some issues. Take the agent's reasons and do a quick dive yourself to decide whether to tackle it or not. If you decide to proceed, launch a new agent and ask it to use the content of that GitHub issue as context.&lt;/p&gt;
&lt;p&gt;Create an &lt;code&gt;AGENTS.md&lt;/code&gt; file to document your architectural preferences and tool patterns. This teaches the coding agent your standards and helps it make better decisions.&lt;/p&gt;
&lt;p&gt;For example, when building Kirin, I started with HTMX+FastAPI but took three iterations to settle on "everything is an API endpoint, but CRUD endpoints must redirect to view endpoints." This also happened to be an architectural pattern that I settled on only after two iterations on the UI. Another pattern that I settled on was to build the Python API first, then reuse it behind web UI APIs, like building the backend API before the frontend. I settled on this pattern after discovering discrepancies between the UI's sluggish performance and the Python API's snappy performance.&lt;/p&gt;
&lt;p&gt;Document your favorite tools and patterns in &lt;code&gt;AGENTS.md&lt;/code&gt;. You can "teach" the agent to use the &lt;code&gt;gh&lt;/code&gt; CLI for GitHub operations rather than doing janky cURL commands. Use this file as a way to encode your development standards.&lt;/p&gt;
&lt;p&gt;For example, you can "teach" it to use the &lt;code&gt;gh&lt;/code&gt; CLI to get issues from GitHub by literally saying "use the &lt;code&gt;gh&lt;/code&gt; cli to get issue contents from this repo's issues" and it'll almost always reliably do so rather than doing janky cURL commands. This is an important part of building out your test harness and development workflow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MCP servers for specialized knowledge:&lt;/strong&gt; Plug in an MCP (Model Context Protocol) server that serves up documentation about core packages or specialized ways of working specific to your organization. This gives the agent access to your internal knowledge base, coding standards, and domain-specific patterns without cluttering the main context window. The agent can then reference this specialized knowledge when making architectural decisions or implementing features.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Custom shortcuts:&lt;/strong&gt; Slash commands are powerful shortcuts for giving textual context to coding agents. Create them freely, delete them freely, and merge them freely. Experiment to see what works with your habits.&lt;/p&gt;
&lt;p&gt;My favorites include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/remember&lt;/code&gt; - Get the agent to remember important information in &lt;code&gt;AGENTS.md&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/branch-and-stage&lt;/code&gt; - Create a new git branch and stage all changes after completing work&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the actual slash command for &lt;code&gt;/branch-and-stage&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;%% /branch-and-stage.md %%
Given everything we just did, or given what you see when you run git diff, give me a new git branch and git add to stage all the changes. You do not need to give me a commit message, I have a git commit message writer.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And for &lt;code&gt;/remember&lt;/code&gt;:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;%% /remember %%
Remember what you just learned (or what I am about to say) by writing it into AGENTS.md.
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can phrase many of these tips as slash commands. The key is making repetitive tasks into simple text shortcuts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;No task is too small:&lt;/strong&gt; Agents work well for mundane tasks that humans find tedious. I have a slash command for markdown linting because I'm that nitpicky, but it proves the point: no task is too mundane for a coding agent, as long as it can access the output as text to verify it did the work correctly.&lt;/p&gt;
&lt;p&gt;This works so well because agents have gotten great at using command line tools, and command line outputs are exactly the kind of interface LLMs need: text. Every git command, every test run, every build process produces text that the agent can read, understand, and act upon.&lt;/p&gt;
&lt;p&gt;Use agents for CI/CD pipeline maintenance. If your CI/CD isn't conditional (running tests on PRs that only touch documentation), get the agent to make PR tests run only on relevant file changes. Make sure the changes are easily reviewable. This is an important part of building out your test harness.&lt;/p&gt;
&lt;p&gt;For example, if your CI/CD is not conditional and you run tests even on PRs that only touch documentation, get the agent to make your PR tests run only on changes to relevant files—source, config, etc., but not on docs.&lt;/p&gt;
&lt;p&gt;For large PRs, ask the agent to give you an overview of contents. Use your tool's "plan" mode to get a first-pass grasp of what's changed. This is especially useful when you have very large PRs to review—start with your tool's "plan" mode to help you get a first-pass grasp of the contents.&lt;/p&gt;
&lt;p&gt;The meta-workflow that works best is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Plan (write as a .md file, save in plans/ directory)&lt;/li&gt;
&lt;li&gt;Execute (write the code)&lt;/li&gt;
&lt;li&gt;Write tests&lt;/li&gt;
&lt;li&gt;Run tests&lt;/li&gt;
&lt;li&gt;Re-execute as necessary until tests pass&lt;/li&gt;
&lt;li&gt;Audit - check the code against the plan&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;You don't need fancy prompts for this. Write out your high-level goals, have the tool write the plan, read the plan back to you, correct its assumptions, then proceed with steps 2-6.&lt;/p&gt;
&lt;p&gt;GIGO (Garbage In, Garbage Out) applies to AI coding just as much as everything else. If you're sloppy and undisciplined, you'll get predictably bad results.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Effective agent usage isn't about finding the perfect prompt. It's about creating systematic workflows that use the agent's strengths while compensating for its weaknesses. It's about building external memory systems that persist across sessions. It's about teaching the agent your standards so it can make better decisions.&lt;/p&gt;
&lt;p&gt;The key is being willing to fail fast, learn from mistakes, and iterate quickly. The agent amplifies your development process, but only if you're disciplined about how you use it.&lt;/p&gt;
&lt;p&gt;Coding agents are becoming standard tools. The question isn't whether they'll replace developers, it's whether you'll learn to use them effectively. These patterns have changed how I approach development, and they can do the same for you.&lt;/p&gt;
</content></entry><entry><title>How to use multiple GitHub accounts on the same computer</title><link href="https://ericmjl.github.io/blog/2025/10/10/how-to-use-multiple-github-accounts-on-the-same-computer/" rel="alternate"/><updated>2025-10-10T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:ecca655b-1380-3661-949f-b51d525785a6</id><content type="html">&lt;h1 id="how-to-use-multiple-github-accounts-on-the-same-computer"&gt;How to use multiple GitHub accounts on the same computer&lt;/h1&gt;&lt;p&gt;I recently ran into a frustrating situation where I couldn't push to a repository even though I had the right permissions. The problem? I was trying to use two different GitHub accounts on the same computer, and Git was getting confused about which account to use.&lt;/p&gt;
&lt;p&gt;If you're in a similar situation - maybe you have a personal account and also contribute to a non-profit or open source project with a separate account - this guide will help you set everything up correctly.&lt;/p&gt;
&lt;h2 id="the-core-problem"&gt;The core problem&lt;/h2&gt;&lt;p&gt;Here's what was happening to me: I had switched my GitHub CLI to my other account using &lt;code&gt;gh auth switch&lt;/code&gt;, but when I tried to push, Git was still authenticating with my personal account's SSH key.&lt;/p&gt;
&lt;p&gt;The issue is that &lt;code&gt;gh auth switch&lt;/code&gt; only changes which account the GitHub CLI uses for API operations. It doesn't affect which SSH key Git uses for push/pull operations. Git and SSH operate independently from the &lt;code&gt;gh&lt;/code&gt; tool.&lt;/p&gt;
&lt;h2 id="what-you-ll-need"&gt;What you'll need&lt;/h2&gt;&lt;p&gt;Two GitHub accounts (I'll call them &lt;code&gt;personal-account&lt;/code&gt; and &lt;code&gt;volunteer-account&lt;/code&gt; in this guide), terminal access, admin permissions on your repositories, and about 10-15 minutes.&lt;/p&gt;
&lt;h2 id="step-1-create-separate-ssh-keys-for-each-account"&gt;Step 1: Create separate SSH keys for each account&lt;/h2&gt;&lt;p&gt;First, we need distinct SSH keys for each account. If you don't already have separate keys, create them:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Create a key for your volunteer account&lt;/span&gt;
ssh-keygen&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;ed25519&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;volunteer-email@example.com&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519_volunteer

&lt;span class="c1"&gt;# Create a key for your personal account (if you don&amp;#39;t have one)&lt;/span&gt;
ssh-keygen&lt;span class="w"&gt; &lt;/span&gt;-t&lt;span class="w"&gt; &lt;/span&gt;ed25519&lt;span class="w"&gt; &lt;/span&gt;-C&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;personal-email@example.com&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519_personal
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;When prompted for a passphrase, you can either set one or leave it empty (though a passphrase is more secure).&lt;/p&gt;
&lt;h2 id="step-2-add-the-ssh-keys-to-your-ssh-agent"&gt;Step 2: Add the SSH keys to your SSH agent&lt;/h2&gt;&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ssh-add&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519_volunteer
ssh-add&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519_personal
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can verify both keys are loaded:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ssh-add&lt;span class="w"&gt; &lt;/span&gt;-l
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="step-3-add-the-public-keys-to-github"&gt;Step 3: Add the public keys to GitHub&lt;/h2&gt;&lt;p&gt;For each account, you need to add its corresponding public key:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Copy your volunteer account&amp;#39;s public key&lt;/span&gt;
cat&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_ed25519_volunteer.pub
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Log into GitHub as your volunteer account&lt;/li&gt;
&lt;li&gt;Go to Settings → SSH and GPG keys → New SSH key&lt;/li&gt;
&lt;li&gt;Paste the public key there&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Repeat this process for your personal account with &lt;code&gt;id_ed25519_personal.pub&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="step-4-configure-ssh-to-use-different-keys-for-different-hosts"&gt;Step 4: Configure SSH to use different keys for different "hosts"&lt;/h2&gt;&lt;p&gt;Edit or create &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# Default GitHub (personal account)
Host github.com
  HostName github.com
  User git
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519_personal

# GitHub for volunteer account
Host github.com-volunteer
  HostName github.com
  User git
  IdentityFile ~/.ssh/id_ed25519_volunteer
  IdentitiesOnly yes
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;Host github.com-volunteer&lt;/code&gt; line creates a local alias that only exists in your SSH config. When Git tries to connect to &lt;code&gt;github.com-volunteer&lt;/code&gt;, SSH will actually connect to &lt;code&gt;github.com&lt;/code&gt; but use the specified SSH key.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; line tells SSH to only use the key you specified and not try other keys from your SSH agent.&lt;/p&gt;
&lt;h2 id="step-5-update-your-repository-s-remote-url"&gt;Step 5: Update your repository's remote URL&lt;/h2&gt;&lt;p&gt;For any repository belonging to your volunteer account, you need to update the remote URL to use the SSH alias:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Navigate to your repo&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/path/to/nonprofit-project

&lt;span class="c1"&gt;# Check current remote&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;-v

&lt;span class="c1"&gt;# Update to use the volunteer account&amp;#39;s SSH config&lt;/span&gt;
git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;set-url&lt;span class="w"&gt; &lt;/span&gt;origin&lt;span class="w"&gt; &lt;/span&gt;git@github.com-volunteer:organization/nonprofit-project.git
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice the change: &lt;code&gt;git@github.com-volunteer:&lt;/code&gt; instead of &lt;code&gt;git@github.com:&lt;/code&gt;. This is necessary because the hostname in the URL is what triggers SSH to look up the configuration in your &lt;code&gt;~/.ssh/config&lt;/code&gt; file. When Git sees &lt;code&gt;github.com-volunteer&lt;/code&gt;, SSH matches it to the &lt;code&gt;Host github.com-volunteer&lt;/code&gt; entry and uses the correct key.&lt;/p&gt;
&lt;h2 id="step-6-test-the-connection"&gt;Step 6: Test the connection&lt;/h2&gt;&lt;p&gt;Before pushing, verify SSH is authenticating correctly:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;-T&lt;span class="w"&gt; &lt;/span&gt;git@github.com-volunteer
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should see:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Hi volunteer-account! You've successfully authenticated, but GitHub does not provide shell access.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If it says your personal account name instead, something's wrong with your SSH config.&lt;/p&gt;
&lt;p&gt;Now try pushing:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="troubleshooting-common-issues"&gt;Troubleshooting common issues&lt;/h2&gt;&lt;h3 id="issue-1-ssh-still-authenticates-with-the-wrong-account"&gt;Issue 1: SSH still authenticates with the wrong account&lt;/h3&gt;&lt;p&gt;If &lt;code&gt;ssh -T git@github.com-volunteer&lt;/code&gt; shows your personal account name instead of your volunteer account, the problem is usually that SSH is trying multiple keys and GitHub is accepting the first one that works.&lt;/p&gt;
&lt;p&gt;Make sure you have &lt;code&gt;IdentitiesOnly yes&lt;/code&gt; in your &lt;code&gt;~/.ssh/config&lt;/code&gt; for the &lt;code&gt;github.com-volunteer&lt;/code&gt; host. This forces SSH to only use the specified key.&lt;/p&gt;
&lt;h3 id="issue-2-could-not-resolve-hostname-github.com-volunteer""&gt;Issue 2: "Could not resolve hostname github.com-volunteer"&lt;/h3&gt;&lt;p&gt;This usually means Git has a custom SSH command configured that's bypassing your SSH config file. Check:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;--get&lt;span class="w"&gt; &lt;/span&gt;core.sshCommand
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If this returns something with &lt;code&gt;-F /dev/null&lt;/code&gt;, that's your problem. The &lt;code&gt;-F /dev/null&lt;/code&gt; flag tells SSH to ignore all config files.&lt;/p&gt;
&lt;p&gt;Remove it:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;--unset&lt;span class="w"&gt; &lt;/span&gt;core.sshCommand
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="issue-3-config-changes-don-t-seem-to-apply"&gt;Issue 3: Config changes don't seem to apply&lt;/h3&gt;&lt;p&gt;If you have conditional Git configs (using &lt;code&gt;includeIf&lt;/code&gt; directives), they might be overriding your settings. Check:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;config&lt;span class="w"&gt; &lt;/span&gt;--list&lt;span class="w"&gt; &lt;/span&gt;--show-origin&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;sshCommand
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This shows you exactly which config file is setting the SSH command. You may need to edit that file directly.&lt;/p&gt;
&lt;p&gt;For example, I had a &lt;code&gt;~/.gitconfig-volunteer&lt;/code&gt; file that was automatically loaded for repos in certain directories, and it had a problematic &lt;code&gt;core.sshCommand&lt;/code&gt; setting that needed to be fixed.&lt;/p&gt;
&lt;h3 id="issue-4-repository-not-found-error"&gt;Issue 4: "Repository not found" error&lt;/h3&gt;&lt;p&gt;This means SSH is connecting and authenticating, but as the wrong account. Double-check:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Run &lt;code&gt;ssh -T git@github.com-volunteer&lt;/code&gt; and verify it shows the correct account name&lt;/li&gt;
&lt;li&gt;Verify the account has access to the repository on GitHub&lt;/li&gt;
&lt;li&gt;Check that your remote URL uses the correct alias: &lt;code&gt;git@github.com-volunteer:org/repo.git&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="optional-set-up-conditional-git-configs"&gt;Optional: Set up conditional Git configs&lt;/h2&gt;&lt;p&gt;If you keep repositories for your volunteer work in a specific directory (like &lt;code&gt;~/volunteer-projects/&lt;/code&gt;), you can automatically apply settings to all repos in that directory.&lt;/p&gt;
&lt;p&gt;Add this to your &lt;code&gt;~/.gitconfig&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[includeIf "gitdir:~/volunteer-projects/"]
    path = ~/.gitconfig-volunteer
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create &lt;code&gt;~/.gitconfig-volunteer&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[user]
    email = volunteer-email@example.com

[core]
    sshCommand = ssh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This automatically sets your volunteer account's email for commits in that directory. The &lt;code&gt;sshCommand&lt;/code&gt; should be set to plain &lt;code&gt;ssh&lt;/code&gt; so it uses your &lt;code&gt;~/.ssh/config&lt;/code&gt; properly.&lt;/p&gt;
&lt;h2 id="how-this-all-works-together"&gt;How this all works together&lt;/h2&gt;&lt;p&gt;When you run &lt;code&gt;git push&lt;/code&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Git reads the remote URL: &lt;code&gt;git@github.com-volunteer:org/repo.git&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Git asks SSH to connect to &lt;code&gt;github.com-volunteer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;SSH looks in &lt;code&gt;~/.ssh/config&lt;/code&gt; and finds the &lt;code&gt;Host github.com-volunteer&lt;/code&gt; entry&lt;/li&gt;
&lt;li&gt;SSH sees it should actually connect to &lt;code&gt;github.com&lt;/code&gt; but use the &lt;code&gt;id_ed25519_volunteer&lt;/code&gt; key&lt;/li&gt;
&lt;li&gt;SSH connects to GitHub with the correct key&lt;/li&gt;
&lt;li&gt;GitHub authenticates you as your volunteer account&lt;/li&gt;
&lt;li&gt;Push succeeds&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each repository uses the correct account automatically based on its remote URL, so you never have to manually specify which key to use.&lt;/p&gt;
&lt;h2 id="wrapping-up"&gt;Wrapping up&lt;/h2&gt;&lt;p&gt;Managing multiple GitHub accounts on the same computer isn't intuitive, but once you understand that Git uses SSH keys (not &lt;code&gt;gh auth&lt;/code&gt; settings), the solution becomes clear. The SSH config host alias pattern is the standard way to handle this, and it works reliably once everything is configured correctly.&lt;/p&gt;
&lt;p&gt;The key points to remember:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SSH keys are what matter for Git operations, not &lt;code&gt;gh auth&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Host aliases in &lt;code&gt;~/.ssh/config&lt;/code&gt; let you use different keys for different repos&lt;/li&gt;
&lt;li&gt;&lt;code&gt;IdentitiesOnly yes&lt;/code&gt; prevents SSH from trying multiple keys&lt;/li&gt;
&lt;li&gt;Your remote URL must use the alias (e.g., &lt;code&gt;git@github.com-volunteer:&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you run into issues, the troubleshooting section above covers the most common problems I encountered.&lt;/p&gt;
</content></entry><entry><title>How to teach your coding agent with AGENTS.md</title><link href="https://ericmjl.github.io/blog/2025/10/4/how-to-teach-your-coding-agent-with-agentsmd/" rel="alternate"/><updated>2025-10-04T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:af4c409b-5ba1-3670-8181-8f2a5e5d8a58</id><content type="html">&lt;p&gt;Let me start with the most valuable thing I learned this week: if there's anything you want your LLM coding agent to remember for future sessions, just tell it to "Please update AGENTS.md with..." and then specify what you want it to remember.&lt;/p&gt;
&lt;p&gt;That's it. That's the meta-tip that changes everything.&lt;/p&gt;
&lt;h2 id="what-is-agents-md-anyway"&gt;What is AGENTS.md anyway&lt;/h2&gt;&lt;p&gt;AGENTS.md is an emerging open standard that's been adopted by over 20,000 repositories on GitHub. Think of it as a README for your AI coding agents—a predictable location where you provide context, instructions, and preferences that your agent needs to work effectively on your project.&lt;/p&gt;
&lt;p&gt;You might think of this as similar to ChatGPT's memory feature, but there's a crucial difference: AGENTS.md is explicitly curated by you. You decide exactly what the agent remembers and how it applies that knowledge. I prefer this approach because it means I have control over what the agent knows, rather than the agent autonomously deciding what to remember about me and my preferences. It's transparent, version-controlled, and intentional.&lt;/p&gt;
&lt;p&gt;The format emerged from collaborative efforts across OpenAI, Google (Jules), Cursor, Factory, and other major players in the AI development space. It's just standard Markdown, which means it's accessible, portable, and fits naturally into any project structure.&lt;/p&gt;
&lt;p&gt;While your README.md is optimized for humans—covering project introductions, contribution guidelines, and quick starts—AGENTS.md serves as machine-readable instructions for your coding agents. Setup commands, testing workflows, coding style preferences, and project-specific conventions all live here.&lt;/p&gt;
&lt;h2 id="training-an-employee-not-programming-a-bot"&gt;Training an employee, not programming a bot&lt;/h2&gt;&lt;p&gt;I was inspired by &lt;a href="https://youtu.be/budTmdQfXYU?si=mRQVEbSDZOPRf-Xm"&gt;NetworkChuck's approach to building Terry&lt;/a&gt;, his N8n automation agent. The philosophy framing he uses is both brilliant and yet practical: you're not programming a bot, you're training an employee.&lt;/p&gt;
&lt;p&gt;In Terry's case, Chuck teaches the agent by continuously updating its system prompt with new instructions and context. The same principle applies perfectly to AGENTS.md in coding environments.&lt;/p&gt;
&lt;p&gt;Here's what makes this powerful: AGENTS.md gets sent with every LLM API call in Cursor, Claude Code, GitHub Copilot, and other modern coding tools. This means you can standardize on AGENTS.md, and as you progress through your project, you effectively teach the LLM what to do by instructing it to update this file with your preferences and learnings.&lt;/p&gt;
&lt;p&gt;The beauty is that these instructions persist across sessions. Your agent doesn't forget; it gets smarter as your project evolves.&lt;/p&gt;
&lt;h2 id="concrete-tip-1-enforce-markdown-standards-automatically"&gt;Concrete tip 1: Enforce markdown standards automatically&lt;/h2&gt;&lt;p&gt;One of my first uses for AGENTS.md was ensuring consistent markdown formatting. I asked my coding agent to update AGENTS.md with instructions to always run markdownlint on any markdown files it creates or edits.&lt;/p&gt;
&lt;p&gt;Here's what I added to the file:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gu"&gt;## Markdown standards&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Always run markdownlint on any markdown files created or edited
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Install using: &lt;span class="sb"&gt;`npx markdownlint-cli`&lt;/span&gt; or &lt;span class="sb"&gt;`pixie global install markdownlint-cli`&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Fix all linting issues before completing the task
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The effect is immediate. Now, anytime my agent writes or edits a markdown file, it automatically runs markdownlint and fixes issues. I don't have to remember to ask for this. The agent just knows it's part of the workflow.&lt;/p&gt;
&lt;h2 id="concrete-tip-2-specify-your-testing-style"&gt;Concrete tip 2: Specify your testing style&lt;/h2&gt;&lt;p&gt;I prefer writing tests as &lt;code&gt;pytest&lt;/code&gt; style functions rather than unittest-style classes. Most LLMs default to the unittest approach because it's more prevalent in their training data.&lt;/p&gt;
&lt;p&gt;So I instructed my agent to add this to AGENTS.md:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gu"&gt;## Testing preferences&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Write all Python tests as &lt;span class="sb"&gt;`pytest`&lt;/span&gt; style functions, not unittest classes
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Use descriptive function names starting with &lt;span class="sb"&gt;`test_`&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Prefer fixtures over setup/teardown methods
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Use assert statements directly, not self.assertEqual
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now when I ask for tests, I consistently get &lt;code&gt;pytest&lt;/code&gt; style functions. The agent is steered toward my preferred approach without me having to specify it in every request.&lt;/p&gt;
&lt;h2 id="concrete-tip-3-stop-writing-throwaway-test-scripts"&gt;Concrete tip 3: Stop writing throwaway test scripts&lt;/h2&gt;&lt;p&gt;Here's a pattern I noticed with Cursor: when the agent wants to test something, it loves to write little throwaway scripts. You know the type—&lt;code&gt;test_random_thing.py&lt;/code&gt; or &lt;code&gt;quick_check.py&lt;/code&gt; that do some ad hoc verification and then just sit there cluttering your project.&lt;/p&gt;
&lt;p&gt;The problem is these scripts aren't real tests—yet they're also tests. They don't run with your test suite. They don't provide ongoing regression protection. They're just... there.&lt;/p&gt;
&lt;p&gt;I taught my agent to write proper tests instead:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gu"&gt;## Testing approach&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Never create throwaway test scripts or ad hoc verification files
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;If you need to test functionality, write a proper test in the test suite
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;All tests go in the &lt;span class="sb"&gt;`tests/`&lt;/span&gt; directory following the project structure
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Tests should be runnable with the rest of the suite (&lt;span class="sb"&gt;`pixi run pytest`&lt;/span&gt;)
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Even for quick verification, write it as a real test that provides ongoing value
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now when the agent needs to verify something works, it writes an actual test that becomes part of the project. These tests continue to provide value by catching regressions, documenting expected behavior, and running in CI.&lt;/p&gt;
&lt;p&gt;The shift is subtle but powerful: instead of creating technical debt in the form of random scripts, you're building up a proper test suite.&lt;/p&gt;
&lt;h2 id="concrete-tip-4-teach-your-agent-about-new-tooling"&gt;Concrete tip 4: Teach your agent about new tooling&lt;/h2&gt;&lt;p&gt;I recently adopted Pixi as my main package manager. The problem? Most LLMs aren't familiar with Pixi commands yet. They kept trying to run &lt;code&gt;python&lt;/code&gt; directly when I only have Python available through Pixi.&lt;/p&gt;
&lt;p&gt;The solution was to teach the agent:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gu"&gt;## Package management&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;This project uses Pixi for all package management
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Never run commands directly (python, pytest, etc.)
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Always prefix commands with &lt;span class="sb"&gt;`pixi run &amp;lt;command&amp;gt;`&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Example: &lt;span class="sb"&gt;`pixi run python script.py`&lt;/span&gt; not &lt;span class="sb"&gt;`python script.py`&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Example: &lt;span class="sb"&gt;`pixi run pytest`&lt;/span&gt; not &lt;span class="sb"&gt;`pytest`&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This works for any new tooling. If you've adopted &lt;code&gt;pixi&lt;/code&gt; or &lt;code&gt;uv&lt;/code&gt; or any other modern Python tools that aren't well-represented in LLM training data, you can explicitly teach your agent how to use them through AGENTS.md.&lt;/p&gt;
&lt;p&gt;The same principle applies to any domain-specific tools or workflows unique to your project. For example, if you're working with Marimo notebooks, which have a relatively strict syntax:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="gu"&gt;## Marimo notebook validation&lt;/span&gt;

&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;After creating or editing any Marimo notebook, always run validation
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Command: &lt;span class="sb"&gt;`uvx marimo check &amp;lt;notebook&amp;gt;.py`&lt;/span&gt;
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Fix any syntax errors reported before completing the task
&lt;span class="k"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Marimo notebooks require strict syntax adherence for proper execution
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now your agent will automatically validate Marimo notebooks and get immediate feedback on syntax errors, ensuring notebooks are written correctly the first time.&lt;/p&gt;
&lt;h2 id="why-this-matters"&gt;Why this matters&lt;/h2&gt;&lt;p&gt;The traditional approach to working with coding agents involves repeating yourself constantly. "Remember to use this format." "Don't forget to run this command." "We prefer this style here."&lt;/p&gt;
&lt;p&gt;AGENTS.md flips this model. Instead of being a human repeating instructions to a forgetful assistant, you're building up institutional knowledge that persists. You're training your agent to work the way you work.&lt;/p&gt;
&lt;p&gt;As one developer observed, "it's all about simple human psychology: You get immediate feedback &amp;amp; results: You write it once, and your AI assistant immediately becomes more useful. The feedback loop is much longer with READMEs."&lt;/p&gt;
&lt;p&gt;This is the key insight. When you write a README, you're creating documentation for a future human reader who may or may not show up. When you write AGENTS.md, you get instant gratification—your next conversation with the agent immediately reflects what you just taught it. The AI won't judge you for weird conventions or hacky workarounds. It just learns and applies what you've documented.&lt;/p&gt;
&lt;p&gt;Each time you discover a preference, a gotcha, or a best practice for your project, you can capture it in AGENTS.md. The next time your agent encounters a similar situation, it already knows what to do.&lt;/p&gt;
&lt;p&gt;This is especially powerful in larger projects or monorepos. You can have AGENTS.md files in subdirectories, and agents will use the nearest file to the code being edited—similar to how .gitignore or ESLint configs work. This lets you provide context-specific instructions for different parts of your codebase.&lt;/p&gt;
&lt;h2 id="getting-started"&gt;Getting started&lt;/h2&gt;&lt;p&gt;If you already have agent instruction files like &lt;code&gt;.cursorrules&lt;/code&gt;, &lt;code&gt;CLAUDE.md&lt;/code&gt;, or &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt;, you can simply rename them to AGENTS.md. Most modern coding agents now support this standard.&lt;/p&gt;
&lt;p&gt;Start simple. Create an AGENTS.md in your project root and add one or two critical preferences. Then, as you work with your agent, whenever you find yourself giving the same instruction twice, add it to AGENTS.md instead.&lt;/p&gt;
&lt;p&gt;The key insight is this: every time you teach your agent something, make it permanent by updating AGENTS.md. That's how you build an agent that truly understands your project.&lt;/p&gt;
</content></entry><entry><title>How data scientists can master life sciences and software skills for biotech using ultralearning</title><link href="https://ericmjl.github.io/blog/2025/10/1/how-data-scientists-can-master-life-sciences-and-software-skills-for-biotech-using-ultralearning/" rel="alternate"/><updated>2025-10-01T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:702334d0-8c72-3386-aa45-8bc48b858216</id><content type="html">&lt;p&gt;After 8 years working in biotech and 6 years of graduate training before that, I've observed something about the most effective data scientists in biotech: they aren't just T- or π-shaped -- posessing breadth in skill while being deep in 1 or 2 specialties. They're continuously learning new skills to bridge their knowledge gaps.&lt;/p&gt;
&lt;p&gt;There are two common knowledge gaps that I've observed. On one side, there's the vast world of life sciences: molecular biology, cell biology, genetics, immunology, neuroscience, analytical chemistry, organic chemistry, biochemistry. On the other, there's software development, the kind of skills that let you build reliable, maintainable tools that actually work in production.&lt;/p&gt;
&lt;p&gt;The challenge is that these two domains are fundamentally different in how you learn them. And here's the thing: you can't just "take courses" in those domains and call it done. The life sciences alone are too vast. You need a strategy for continuous, rapid learning in both domains over your entire career.&lt;/p&gt;
&lt;p&gt;When I interview data scientists for biotech roles, I assess five key areas: people skills, communication skills, scientific knowledge, coding skills, and modeling skills. The two domains I'm talking about here — life sciences and software development — map directly to scientific knowledge and coding skills. These aren't just nice-to-haves; they're essential for effectiveness in biotech data science.&lt;/p&gt;
&lt;p&gt;That's where "ultralearning" comes in. It's Scott Young's framework for aggressive, self-directed learning, and I know it works because I've lived it. I started as a bench scientist but taught myself computing, software development, and machine learning over the years. Now I want to show you how data scientists in biotech can do the same—whether you're learning domain knowledge or software skills.&lt;/p&gt;
&lt;p&gt;How do you strategically build depth in both life sciences and software over time? I'm going to walk through the 9 principles of ultralearning that Scott Young outlines and show you how they map to learning both domains for biotech data science. I've reordered them in a way that builds momentum, starting with what matters most.&lt;/p&gt;
&lt;h2 id="the-9-principles"&gt;The 9 principles&lt;/h2&gt;&lt;h3 id="principle-3-directness-learn-by-doing-the-real-thing"&gt;Principle 3: Directness - learn by doing the real thing&lt;/h3&gt;&lt;p&gt;Starting with principle 3, here's what directness means: you learn most viscerally in the actual context where you'll apply the skill. And I'm putting this first because it's where most people go wrong.&lt;/p&gt;
&lt;p&gt;Most people read textbooks and take courses. This isn't bad in and of itself, but if you assume that covering the material means you have learned it, you are wrong. Without a context to apply it, the knowledge doesn't stick. You need a real project where you actually use what you're learning.&lt;/p&gt;
&lt;p&gt;If you're already working in biotech, you have a huge advantage: you already have real projects with real stakes. These projects naturally focus your learning because you have a job to be done! This is why I put directness first: it leverages the learning environment you already have.&lt;/p&gt;
&lt;p&gt;For learning life sciences, this means treating your current project as your learning laboratory. You're analyzing RNA-seq data? Learn the biology behind the genes you're seeing, then immediately apply that knowledge to interpret your results and suggest follow-up experiments. You're working with metabolomics data? Learn the metabolic pathways, then use that understanding to identify which metabolites are actually biologically meaningful versus technical artifacts. You're building models for drug discovery? Learn the specific organic chemistry that you're working with, then apply it to explain why your model predicts certain compounds will work and others won't, and use that reasoning to guide your next round of experiments.&lt;/p&gt;
&lt;p&gt;Your current project is your learning laboratory. Treat the scientific knowledge gaps you encounter as targets for deep learning.&lt;/p&gt;
&lt;p&gt;And here's something I've noticed: when you write internal documentation or reports, that's actually retrieval practice for the science you're learning. More on retrieval later, but the point is your work gives you built-in learning opportunities if you use them intentionally.&lt;/p&gt;
&lt;p&gt;The same applies to learning software. Your pipeline is getting slow? Learn performance optimization and profiling, then immediately apply those techniques to identify bottlenecks and speed up your actual pipeline. Your code is getting hard to maintain? Learn design patterns, then refactor your existing codebase using those patterns to make it more modular and testable. You need to deploy something? Learn containerization and orchestration, then use those skills to get your tool running in production and accessible to your team.&lt;/p&gt;
&lt;p&gt;Your work projects provide the constraints and requirements that make software concepts meaningful. When you document your code or write design docs, you're forced to articulate the architectural decisions you're making—and that's when you really learn them.&lt;/p&gt;
&lt;h3 id="principle-4-drill-isolate-and-attack-weak-points"&gt;Principle 4: Drill - isolate and attack weak points&lt;/h3&gt;&lt;p&gt;Drilling is about identifying rate-limiting steps in your skills and practicing them specifically. Once you're doing direct practice through your projects, you'll notice where you keep getting stuck.&lt;/p&gt;
&lt;p&gt;Here, there is a meta-skill that I think is critical: self-awareness. You need to develop the ability to notice when you're missing context, without someone explicitly telling you. When you're reading a paper and realize you're lost, or when you're in a meeting and can't follow the reasoning, that's your signal. Learning to recognize these moments yourself is what makes drilling effective.&lt;/p&gt;
&lt;p&gt;For learning life sciences, drilling means identifying the specific skills that are blocking your work and practicing them repeatedly. Do you keep having to ask biologists what ChIP-seq tells you? Drill by practicing biological interpretation: take 20 different ChIP-seq results and explain what each peak means biologically—what transcription factor is binding, what genes are being regulated, and what biological process is affected. Are you on a project with immunologists but can't follow their reasoning? Drill by practicing experimental logic: read 15 immunology papers and predict what the next experiment should be based on the current results, then check your reasoning against what the authors actually did, or consult an immunologist colleague.&lt;/p&gt;
&lt;p&gt;Or perhaps consider the chemistry side of things. Are you analyzing mass spec data but you're shaky on ionization and fragmentation? Drill by working through 50 fragmentation problems: given a compound structure, predict the top 5 fragments you'd expect to see, then check your answers. Is your lack of organic chemistry preventing you from understanding drug modifications? Drill by practicing modification effects: take 30 different drug structures, make specific modifications (add methyl group, change functional group), and predict how each change would affect binding affinity, solubility, and metabolic stability.&lt;/p&gt;
&lt;p&gt;The key is identifying the real bottleneck in your knowledge and then designing a drill around that. If you have no good priors on how to do this, you should actually be asking your colleagues who have domain knowledge. They can help you pinpoint exactly what you're missing and suggest focused practice exercises.&lt;/p&gt;
&lt;p&gt;For learning software, the same principle applies. Do your pipelines keep breaking in production? Drill by practicing debugging: take your broken pipeline, run it locally with the same data that failed in production, and systematically test each step until you find the exact failure point. Are you blocked on deployment because of containerization issues? Drill this skill while debugging! Start with a minimal Dockerfile that just runs your script, then gradually add dependencies one by one until it works. Strip it back to bare minimum and rebuild it piece by piece, and your understanding will follow too.&lt;/p&gt;
&lt;p&gt;Code review keeps catching the same issues in your code? Drill on those patterns by refactoring your existing code to avoid them. Take one function at a time and rewrite it to follow the patterns your reviewers want. Do you avoid writing tests because you don't really understand testing frameworks? Start by adding a single test to your existing codebase, then gradually add more tests to the functions you use most.&lt;/p&gt;
&lt;p&gt;Identify the one software skill that's limiting your effectiveness right now, and drill it until it's not a bottleneck anymore. Maybe it's Git workflows that are slowing your team down, or packaging that's preventing tool distribution.&lt;/p&gt;
&lt;h3 id="principle-1-metalearning-map-the-territory-first"&gt;Principle 1: Metalearning - map the territory first&lt;/h3&gt;&lt;p&gt;Metalearning means researching how to learn something before diving in. I know it seems backwards to list this third when it's literally the first principle in Scott Young's framework, but here's why: directness and drilling are more immediately actionable. Once you're doing those, metalearning helps you be more strategic about what you're doing.&lt;/p&gt;
&lt;p&gt;For learning life sciences, this means understanding what you actually need to know for your work before diving into intensive learning. Is your team starting a new project in spatial transcriptomics? Before diving in, map out what you need: tissue biology, imaging concepts, the technology itself, analysis methods. Are you joining a drug discovery project? Identify the hierarchy of knowledge; do you need medicinal chemistry basics first, or can you start with binding assays and learn backward?&lt;/p&gt;
&lt;p&gt;Talk to the biologists or chemists you work with. Ask them what foundational concepts matter most for understanding their work. Look at the papers your team references most—what scientific knowledge do they assume? That's your map.&lt;/p&gt;
&lt;p&gt;Find the best resources for your specific need. Maybe it's that one review paper, or a specific textbook chapter, or that scientist down the hall who explains things well. Don't waste time learning areas that aren't relevant to your current work. Map what matters now.&lt;/p&gt;
&lt;p&gt;Additionally, be prepared to re-map! After working on a project for a while, you might find your initial map was wrong. That's totally OK! If you're making errors because you misunderstood what you needed to learn, that's a signal to step back and reassess. An incorrect map is worse than no map.&lt;/p&gt;
&lt;p&gt;For learning software, the same applies. Before diving into a new software skill, understand what good looks like in your specific context—biotech and scientific computing. Your team wants to adopt a new workflow system? Before learning it, map out what you need: workflow concepts, the specific tool's paradigms, container knowledge. Or can you start with examples?&lt;/p&gt;
&lt;p&gt;Look at mature tools in your space — &lt;code&gt;scikit-bio&lt;/code&gt;, &lt;code&gt;scanpy&lt;/code&gt;, or established pipelines like those from the ENCODE project. What patterns do they use? Are they functional-first or objects-first? And what are patterns in how they design their APIs? That's your north star. Talk to experienced engineers if you have access and ask what software skills actually matter for scientific tools.&lt;/p&gt;
&lt;p&gt;The key is understanding the learning path dependencies: do you need to understand Python packaging before you can learn about CI/CD? Or can you learn them together? Map out the shortest path to being effective, not the most comprehensive path to expertise. Focus on what will unblock your current work, not what would make you an expert in everything.&lt;/p&gt;
&lt;h3 id="principle-6-feedback-get-signal-on-your-progress"&gt;Principle 6: Feedback - get signal on your progress&lt;/h3&gt;&lt;p&gt;Feedback is getting useful information about what you're doing wrong and how to fix it. And in biotech, you have built-in feedback mechanisms if you use them intentionally.&lt;/p&gt;
&lt;p&gt;For learning life sciences, leverage the scientists you work with as your feedback mechanism. When you present results in team meetings and biologists or chemists correct your interpretation, that's high-value feedback. Pay attention!&lt;/p&gt;
&lt;p&gt;Join journal clubs if your company has them. When you misunderstand a paper, someone will point it out. If your company doesn't have journal clubs, look in your local community—Boston has several industry-focused options including &lt;a href="https://biotechtuesday.com/category/geographic-region-event/boston-ma/"&gt;BiotechTuesday&lt;/a&gt;, &lt;a href="https://massbio.microsoftcrmportals.com/event/?event=The_Science_of_Biotech_November"&gt;MassBio events&lt;/a&gt;, and &lt;a href="https://cambridgebiotechclub.org/"&gt;Cambridge Biotech Club&lt;/a&gt; networking events that often include research discussions.&lt;/p&gt;
&lt;p&gt;When you write up results or make slides, ask a scientist to review. Where they add clarifications shows your knowledge gaps. If your predictions about experimental outcomes are wrong, that's feedback about your biological understanding. I remember being challenged by a colleague while on a call with external collaborators, and that was the best feedback I had being wrong in a "public" setting! When you explain your interpretation to a biologist and they look confused, you've either misunderstood the science or can't articulate it yet.&lt;/p&gt;
&lt;p&gt;Your work products -- analyses, reports, presentations -- are opportunities to get feedback on your scientific understanding. Don't just present results. Explain your biological reasoning and see where it's challenged.&lt;/p&gt;
&lt;p&gt;For learning software, code review is your primary feedback mechanism. Take it seriously. The comments show you what you don't yet understand.&lt;/p&gt;
&lt;p&gt;Does your code actually work at scale with real-sized data? That's feedback on your software design. When someone else tries to use your tool and files issues, those edge cases reveal gaps in your software thinking. Pair programming with more experienced engineers shows you patterns you're not seeing.&lt;/p&gt;
&lt;p&gt;When onboarding a new team member to your code takes too long, that's feedback that your architecture or documentation needs work. Production failures are harsh but clear feedback: what software concepts do you need to learn to prevent them?&lt;/p&gt;
&lt;p&gt;Ask for architectural review before building something big. Feedback up front prevents expensive mistakes.&lt;/p&gt;
&lt;h3 id="principle-5-retrieval-test-yourself-actively"&gt;Principle 5: Retrieval - test yourself actively&lt;/h3&gt;&lt;p&gt;Retrieval is about actively recalling information, which strengthens learning more than passive review. And I want to emphasize something specific about this for life sciences learning.&lt;/p&gt;
&lt;p&gt;The vocabulary in life sciences is vast, and the meanings of everyday words often change in scientific contexts. Think about "competent" cells, "naive" T-cells, "promiscuous" enzymes, "housekeeping" genes. Good memory for vocabulary isn't just about rote memorization; it gives you the ability to name and label entities clearly, which is foundational even when your primary goal is understanding concepts.&lt;/p&gt;
&lt;p&gt;For learning life sciences, retrieval practice happens naturally in your work if you let it. When you're preparing a presentation for your team, try to explain the biological mechanism from memory first, then check your understanding. Before looking up that pathway or reaction mechanism again, try to draw it from memory. Where you get stuck shows what you haven't really learned.&lt;/p&gt;
&lt;p&gt;In meetings when discussing results, attempt to explain the biology without your notes. This reveals what you actually know versus what you've just read. When writing internal documentation about your project, explain the scientific concepts from memory, then verify. If you're presenting at journal club, practice explaining the paper's biology without constantly referring to slides.&lt;/p&gt;
&lt;p&gt;The act of trying to recall forces your brain to strengthen those neural pathways. Passive rereading doesn't do this. Your work already gives you retrieval opportunities—presentations, documentation, discussions with scientists. Use them.&lt;/p&gt;
&lt;p&gt;For learning software, the same principle applies. When you're about to look up how to do something in code, try to write it from memory first, then look it up if needed. Before copying a design pattern from Stack Overflow, try to implement it based on what you remember, then refine.&lt;/p&gt;
&lt;p&gt;In code review or design discussions, explain your architectural decisions from memory. If you can't, you don't really understand them yet. When documenting your code, write the explanation without constantly referencing the implementation. Try to debug issues by reasoning through the system before looking at logs—this builds your mental model.&lt;/p&gt;
&lt;p&gt;And here's something important: making it easy by constantly looking things up or always relying on AI to spoonfeed you answers is a surefire way to keep knowledge shallow. The struggle of trying to remember is what creates learning.&lt;/p&gt;
&lt;p&gt;This is especially true with AI-generated documentation. You can use AI to generate documentation, but the retrieval practice happens during review. When AI writes "this function calculates the binding affinity," question it: "Does it really? What's the actual algorithm? What are the inputs and outputs?" Challenge each line the AI wrote by trying to explain it from your own understanding. If you can't explain why a particular line is there or what it does, that's your signal to dig deeper into that concept.&lt;/p&gt;
&lt;h3 id="principle-2-focus-cultivate-deep-concentration"&gt;Principle 2: Focus - cultivate deep concentration&lt;/h3&gt;&lt;p&gt;Focus is about managing procrastination, distraction, and maintaining sustained attention. And this matters more than you might think for both domains.&lt;/p&gt;
&lt;p&gt;For learning life sciences, reading that dense Nature paper about a pathway relevant to your project requires deep, uninterrupted focus. You can't do it between Slack messages and meetings -- block time, and shut off communication channels. Understanding complex scientific concepts, such as metabolic regulation, signaling cascades, reaction mechanisms, requires holding multiple pieces in your head simultaneously. Context switching destroys this.&lt;/p&gt;
&lt;p&gt;Block time on your calendar specifically for deep scientific learning. Treat it like a critical meeting. The 15 minutes before standup isn't enough to understand that review paper you need to read. When you're learning a new biological domain for a project, protect longer blocks of focused time for it.&lt;/p&gt;
&lt;p&gt;Your brain needs sustained attention to build the mental models that make scientific knowledge useful, not just memorized.&lt;/p&gt;
&lt;p&gt;For learning software, the same applies. Understanding a complex codebase or debugging a tricky issue requires uninterrupted deep work. You can't do it effectively in fragments. Reading source code of mature projects—like &lt;code&gt;scikit-bio&lt;/code&gt;, &lt;code&gt;scanpy&lt;/code&gt;, or established pipelines—requires sustained attention to follow design decisions.&lt;/p&gt;
&lt;p&gt;Designing a new system architecture requires holding the entire design in your head. That's impossible with constant interruptions. Block time for focused software learning and development, not just cramming it into gaps. The cognitive load of building mental models for software systems is high. Protect that learning time.&lt;/p&gt;
&lt;p&gt;If you're learning a new framework or pattern for work, give it dedicated focus time, not scattered moments. It'll pay dividends many-fold over an entire technical career.&lt;/p&gt;
&lt;h3 id="principle-9-experimentation-explore-beyond-the-beaten-path"&gt;Principle 9: Experimentation - explore beyond the beaten path&lt;/h3&gt;&lt;p&gt;Experimentation is about trying new approaches, methods, and perspectives as you gain proficiency. This becomes more important as you build your foundation in both domains.&lt;/p&gt;
&lt;p&gt;For learning life sciences, as your foundational knowledge grows, start exploring adjacent domains that come up in your work. You're strong in genomics now? When an immunology opportunity comes up in a cross-functional meeting, that's your trigger to explore that domain.&lt;/p&gt;
&lt;p&gt;Try different ways of learning. Sometimes a textbook works, sometimes talking to the scientist at the next desk works better—especially if they're socratically coaching you. Sometimes it's working through a dataset. This reflects a key insight from ultralearning: there's no one-size-fits-all learning format. What works for learning genomics might not work for learning immunology, and what works for you might not work for your colleague. Experiment to find your optimal learning approach for each domain.&lt;/p&gt;
&lt;p&gt;Use insights from one domain to inform another. Cell signaling patterns you learned in neuroscience might help you understand what you're seeing in immunology data, since all cells have singaling pathways. As you work with both biologists and chemists, start connecting how chemical principles inform biological mechanisms. This cross-domain connection is a well-established way to improve retention and deepen understanding.&lt;/p&gt;
&lt;p&gt;Additionally, try experimenting with how you organize and retain scientific knowledge. What works for you personally? This exploration leads to developing your unique perspective, especially on how you synthesize biological and chemical knowledge differently than others.&lt;/p&gt;
&lt;p&gt;For learning software skills, as you gain proficiency, experiment with different approaches to the same problem at work. For example, have you been using conda to manage your Python environments? When you have time, try managing your environment with pixi instead to understand the tradeoffs.&lt;/p&gt;
&lt;p&gt;Try different testing strategies on real work projects to see what catches bugs most effectively for your use case. Experiment with different architectural patterns when building new tools—learn through direct comparison.&lt;/p&gt;
&lt;p&gt;As you grow, you'll develop engineering judgment: knowing when to use which approach, which rules to follow, which to break. This experimentation leads to finding your own effective patterns, not just copying what others do.&lt;/p&gt;
&lt;h3 id="principle-8-intuition-develop-deep-understanding"&gt;Principle 8: Intuition - develop deep understanding&lt;/h3&gt;&lt;p&gt;Intuition is about building mental models of how things actually work, not just memorizing. And this is where the real payoff comes in both domains.&lt;/p&gt;
&lt;p&gt;For learning life sciences, the drilling and retrieval practice we discussed earlier builds the mental models that become intuition. When you drill on fragmentation patterns and then retrieve that knowledge while analyzing mass spec data, you're doing more than mere memorizing: you're building understanding of how molecules break apart. When you practice explaining biological mechanisms from memory and get feedback, you develop the mechanistic reasoning that becomes intuition.&lt;/p&gt;
&lt;p&gt;This intuition lets you reason about new situations you haven't seen before. You can predict whether an experimental approach will work or identify when results don't make biological sense. The goal isn't encyclopedic knowledge, but rather to develop the ability to reason about biological and chemical systems!&lt;/p&gt;
&lt;p&gt;For learning software, the same principle applies. When you drill on design patterns by implementing them from memory, and follow it up by getting feedback through code review, you build understanding of what problems they solve and their tradeoffs. This develops the engineering judgment to know when to use which approach, which rules to follow. And once you know the rules, you know which ones can be broken :-).&lt;/p&gt;
&lt;p&gt;At the end of the day, intuition develops through the active practice of drilling and retrieval, and not through passive consumption of information. Keep that in mind!&lt;/p&gt;
&lt;h3 id="principle-7-retention-don-t-let-knowledge-leak-away"&gt;Principle 7: Retention - don't let knowledge leak away&lt;/h3&gt;&lt;p&gt;Retention is about understanding why we forget and using strategies to remember long-term. And this matters because you're constantly encountering new concepts in both domains.&lt;/p&gt;
&lt;p&gt;For learning life sciences, you're constantly encountering new biological and chemical concepts at work. Without retention strategies, you'll keep relearning the same things. When you write internal documentation or reports, you're creating reference material for future you and your team—but only if you structure it for easy retrieval and review. Create a personal knowledge base with clear tags and cross-references so you can quickly find concepts when they come up again. I use Obsidian for my personal work knowledge base; it is centered around projects, but I also curate and link facts in there.&lt;/p&gt;
&lt;p&gt;The knowledge you use regularly will naturally stick through repeated exposure. But concepts from past projects will fade without reinforcement. Identify which scientific knowledge you need for the long-term versus what you need just for this project. For long-term retention, create Anki decks for key terminology and mechanisms that keep appearing across different projects. With Obsidian, drilling with an spaced repetition is possible with the &lt;a href="https://www.stephenmwangi.com/obsidian-spaced-repetition/"&gt;Spaced Repetition&lt;/a&gt; plugin.&lt;/p&gt;
&lt;p&gt;Connect new concepts to existing knowledge from your work—this creates stronger memory traces. When you learn a new pathway, relate it to ones you already know. When you encounter a new protein, link it to similar ones you've worked with. Revisit foundational concepts periodically as they show up in different projects, and update your notes and records each time you work on a project.&lt;/p&gt;
&lt;p&gt;The scientific knowledge you use regularly in your work will stick. Everything else needs deliberate retention strategies.&lt;/p&gt;
&lt;p&gt;For learning software, patterns you don't use regularly will fade. Be deliberate about which ones you need to retain. Writing documentation about the systems you build serves as external memory you can reference later, but make it searchable and well-organized to facilitate retrieval later! Create code snippets and examples for patterns you want to remember, and archive them in your work knowledge vault, or share them with colleagues on shared documentation platforms like Confluence or Notion.&lt;/p&gt;
&lt;p&gt;The tools and patterns you use daily will stick naturally through repeated exposure. But specialized knowledge from past projects will fade without reinforcement. If you're not writing tests regularly in your work, you'll forget testing patterns. Find ways to practice what you need to retain: contribute to open source projects, build side projects, or create practice exercises.&lt;/p&gt;
&lt;p&gt;Connect new software concepts to existing knowledge from your work. When you learn a new framework, relate it to ones you already know. When you encounter a new design pattern, link it to similar patterns you've used. Contributing to the same codebase over time builds deep, lasting knowledge of its architecture through repeated exposure. When you learn something new for a project, consider whether it's one-time knowledge or something you'll need repeatedly. Prioritize retention for the latter.&lt;/p&gt;
&lt;h2 id="bringing-it-together"&gt;Bringing it together&lt;/h2&gt;&lt;p&gt;These principles reinforce each other when learning both life sciences and software. But here's the key insight I want to emphasize: if you feel pressure to ultralearn both domains simultaneously, the answer is an emphatic "no".&lt;/p&gt;
&lt;p&gt;Instead, you cycle between domains based on what's blocking you. When a scientific knowledge gap prevents you from making progress, whether it's immunology, organic chemistry, or protein biochemistry, you shift into intensive life sciences learning mode using these principles. When software limitations hold you back, you focus there.&lt;/p&gt;
&lt;p&gt;Over years, this creates deep expertise in both domains. Not through divided attention, but through strategic, focused learning periods in each.&lt;/p&gt;
&lt;p&gt;Here's the cycle I've seen work: work with real problems using directness, identify gaps in whichever domain is limiting you through drilling, focus intensively on that domain, get feedback from domain experts, test yourself through writing and building using retrieval, develop intuition, retain through continued practice, then identify the next limiting domain and cycle back.&lt;/p&gt;
&lt;p&gt;At a higher level, there's an interplay between the domains that makes this work. Scientific understanding informs what software to build and what analyses matter. Software skills enable you to answer scientific questions and build tools others can use. They feed each other.&lt;/p&gt;
&lt;p&gt;This approach beats traditional "take courses in both fields" for biotech data scientists because both domains are too vast to learn all at once. Ultralearning gives you a framework for continuous, targeted learning throughout your career. Remember, your goal is not to become a PhD scientist or a senior software engineer, but to build deep enough understanding in both to be effective at the intersection.&lt;/p&gt;
&lt;h2 id="conclusion-and-next-steps"&gt;Conclusion and next steps&lt;/h2&gt;&lt;p&gt;You don't need to apply all 9 principles to both domains at once. In fact, you shouldn't.&lt;/p&gt;
&lt;p&gt;Start with directness in whichever domain is currently limiting your effectiveness. If you can't interpret your results because you don't understand the biology or chemistry, focus there intensively for the next few months. If you can't scale your analyses or build reliable tools because of software gaps, focus there intensively.&lt;/p&gt;
&lt;p&gt;Add feedback loops from experts in that domain. Build from there using the other principles.&lt;/p&gt;
&lt;p&gt;Then, when you've made real progress, identify which domain is now the bottleneck and shift your intensive learning there.&lt;/p&gt;
&lt;p&gt;This is a career-long journey of alternating deep dives, not a sprint to learn everything at once. The most effective biotech data scientists I know are continuously learning in both domains, but wisely: one intensive focus at a time.&lt;/p&gt;
&lt;p&gt;Here's your actionable takeaway: Right now, which domain is most limiting your effectiveness? Pick one concrete gap in that domain. Spend the next month using ultralearning principles to address that specific gap. Only that one. Master it, then reassess.&lt;/p&gt;
&lt;p&gt;You can do it!&lt;/p&gt;
</content></entry><entry><title>The Data Science Bootstrap Notes: A major upgrade for 2025</title><link href="https://ericmjl.github.io/blog/2025/9/2/the-data-science-bootstrap-notes-a-major-upgrade-for-2025/" rel="alternate"/><updated>2025-09-02T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:808862ff-19aa-390b-bbf2-8e9b08358094</id><content type="html">&lt;p&gt;After 8 years since the first edition, I've completely overhauled &lt;em&gt;The Data Science Bootstrap Notes&lt;/em&gt; to reflect the dramatic changes in the Python data science ecosystem. What started as a collection of Obsidian notes has evolved into a comprehensive, modern guide that addresses the tools and practices that actually matter in 2025.&lt;/p&gt;
&lt;h2 id="from-obsidian-notes-to-a-proper-book"&gt;From Obsidian notes to a proper book&lt;/h2&gt;&lt;p&gt;The most visible change is the format itself. The original version existed as a navigable knowledge base in Obsidian, mimicking &lt;a href="https://notes.andymatuschak.org/About_these_notes"&gt;Andy Matuschak's online notes&lt;/a&gt;. While that format was intellectually interesting to make, I found that after years of experimentation, simple was indeed better than cool. The new version uses MkDocs to create a clean, linear book format that's easier to navigate and more accessible to readers.&lt;/p&gt;
&lt;p&gt;But the real transformation goes far deeper than just the presentation layer.&lt;/p&gt;
&lt;h2 id="the-tooling-revolution-conda-pip-pixi-uv"&gt;The tooling revolution: conda + pip → pixi + uv&lt;/h2&gt;&lt;p&gt;The biggest shift in my recommendations centers around environment management. In 2017, conda was the obvious choice for Python data science environments. Today, that's no longer the case.&lt;/p&gt;
&lt;h3 id="enter-pixi-the-environment-management-multi-tool"&gt;Enter pixi: the environment management multi-tool&lt;/h3&gt;&lt;p&gt;I've completely replaced conda with &lt;code&gt;pixi&lt;/code&gt;, a modern environment manager written in Rust that solves many of the fundamental problems that plagued the conda ecosystem. The key advantages are:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatic Lock Files&lt;/strong&gt;: Pixi automatically generates and maintains lock files (&lt;code&gt;pixi.lock&lt;/code&gt;) every time you modify your environment. This solves the critical "it works on my machine" problem that conda users faced when environments would drift over time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Feature-Based Environments&lt;/strong&gt;: Instead of creating separate environments for each purpose, pixi lets you define reusable "features" that can be combined into different environments. You can have &lt;code&gt;tests&lt;/code&gt;, &lt;code&gt;docs&lt;/code&gt;, &lt;code&gt;notebook&lt;/code&gt;, and &lt;code&gt;cuda&lt;/code&gt; features that combine into purpose-built environments like &lt;code&gt;default&lt;/code&gt;, &lt;code&gt;docs&lt;/code&gt;, or &lt;code&gt;cuda&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Task Automation&lt;/strong&gt;: Pixi enables you to replace Makefiles with tasks defined in &lt;code&gt;pyproject.toml&lt;/code&gt;. Commands like &lt;code&gt;pixi run test&lt;/code&gt; or &lt;code&gt;pixi run docs&lt;/code&gt; standardize common operations across your team.&lt;/p&gt;
&lt;h3 id="uv-the-python-tool-manager"&gt;uv: the Python tool manager&lt;/h3&gt;&lt;p&gt;Complementing pixi is &lt;code&gt;uv&lt;/code&gt;, an extremely fast Python package installer and resolver written in Rust. UV handles global tool installation by automatically creating isolated environments for each tool, giving you the convenience of global tools without the mess of a global Python installation.&lt;/p&gt;
&lt;p&gt;This means you can run tools like &lt;code&gt;llamabot&lt;/code&gt; or my own &lt;code&gt;pyds-cli&lt;/code&gt; without worrying about dependency conflicts. The &lt;code&gt;uvx&lt;/code&gt; command even lets you run tools without installing them first.&lt;/p&gt;
&lt;h2 id="modern-project-scaffolding"&gt;Modern project scaffolding&lt;/h2&gt;&lt;p&gt;The new edition introduces &lt;code&gt;pyds-cli&lt;/code&gt;, my opinionated tooling for data scientists that scaffolds new projects using cookiecutter and pixi. Instead of manually setting up project structures, you can now run:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;pyds&lt;span class="w"&gt; &lt;/span&gt;project&lt;span class="w"&gt; &lt;/span&gt;init
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This creates a complete project structure with proper environment management, testing setup, documentation configuration, and CI/CD pipelines already configured.&lt;/p&gt;
&lt;h2 id="ai-integration-beyond-the-hype"&gt;AI integration beyond the hype&lt;/h2&gt;&lt;p&gt;Generative AI has fundamentally changed how I think about data science workflows. The new edition includes a comprehensive chapter on working with AI tools that goes beyond simple code generation to address:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The speed of thought&lt;/strong&gt;: AI tools help bridge the gap between how fast we can think and how fast we can type. There's fascinating research showing humans process information at $10^9$ bits/second but think at only 10 bits/second - AI helps bridge this massive gap.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The right kind of lazy&lt;/strong&gt;: I distinguish between being "Bill Gates lazy" (finding efficient ways to work) and being intellectually lazy (blindly trusting AI outputs). You must maintain intellectual responsibility.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Effective patterns&lt;/strong&gt;: I share specific strategies for structuring AI interactions, from starting with the big picture to rapid iteration and verification. This includes the "fat finger sketch" approach where you outline what you want before asking AI to fill in details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Beyond code&lt;/strong&gt;: AI tools are particularly valuable for documentation acceleration, code review assistance, and learning new libraries or techniques.&lt;/p&gt;
&lt;p&gt;The key insight is that AI should amplify our capabilities, not replace our judgment. We need to develop a mindset that embraces these tools while maintaining intellectual rigor.&lt;/p&gt;
&lt;h2 id="ci/cd-and-automation"&gt;CI/CD and automation&lt;/h2&gt;&lt;p&gt;The new edition heavily emphasizes GitHub Actions for continuous integration and deployment. Instead of manual processes, you now have trigger-able bots that can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run tests automatically on every commit&lt;/li&gt;
&lt;li&gt;Build and deploy documentation&lt;/li&gt;
&lt;li&gt;Validate code quality with pre-commit hooks&lt;/li&gt;
&lt;li&gt;Deploy applications to various environments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This automation eliminates the drudgery that often accompanies data science projects and ensures consistency across team members. I've even applied this philosophy to the book itself; the entire publishing process is automated through GitHub Actions that build and deploy the website, while simultaneously updating the Leanpub version with every commit.&lt;/p&gt;
&lt;h2 id="philosophical-foundations"&gt;Philosophical foundations&lt;/h2&gt;&lt;p&gt;While the tools have changed dramatically, the core philosophies remain the same but are now more clearly articulated:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Know Your Compute Stack&lt;/strong&gt;: Deep understanding of your tools enables informed choices about what to automate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Single Source of Truth&lt;/strong&gt;: Establish clear, unambiguous sources for data, code, and configuration&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automate Relentlessly&lt;/strong&gt;: Invest in automation to eliminate repetitive tasks&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Categorize Everything&lt;/strong&gt;: Organize projects using logical categories that make maintenance easier&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;These principles now have concrete implementations through modern tooling, making them more actionable than ever.&lt;/p&gt;
&lt;h2 id="what-s-been-removed"&gt;What's been removed&lt;/h2&gt;&lt;p&gt;Not everything made the cut. I've removed outdated advice about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manual conda environment management (replaced with pixi automation)&lt;/li&gt;
&lt;li&gt;Complex conda-specific workflows (simplified with pixi features)&lt;/li&gt;
&lt;li&gt;Manual lock file generation (now automatic with pixi)&lt;/li&gt;
&lt;li&gt;Manual project scaffolding (now automated with pyds-cli)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-path-forward"&gt;The path forward&lt;/h2&gt;&lt;p&gt;The new edition is designed to get you started quickly while building foundations that scale. It's not just a reference guide; it's a roadmap for establishing practices that grow with your ambitions. The tools and practices I recommend today are the ones I actually use in production, not just theoretical best practices.&lt;/p&gt;
&lt;p&gt;What excites me most about this upgrade is how it addresses the real pain points that data scientists face in 2025. Instead of wrestling with environment conflicts, you're now thinking about how to compose features into purpose-built environments. Instead of manually setting up projects, you're focusing on the actual analysis. Instead of fighting with dependency resolution, you're building reproducible workflows that work the same way for everyone on your team.&lt;/p&gt;
&lt;p&gt;The data science ecosystem has matured significantly since 2017, and this new edition reflects that maturity. It's about getting started the right way; establishing foundations that won't crumble as your projects grow in complexity and team size.&lt;/p&gt;
&lt;p&gt;You can read the book online at &lt;a href="https://ericmjl.github.io/data-science-bootstrap-notes/"&gt;the GitHub Pages site&lt;/a&gt;, and if you prefer a linear reading experience, there's also an &lt;a href="https://leanpub.com/dsbootstrap/"&gt;eBook version on LeanPub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The future of data science is automated, reproducible, and collaborative. This new edition shows you how to get there.&lt;/p&gt;
</content></entry><entry><title>How to use AI to accelerate your career in 2025</title><link href="https://ericmjl.github.io/blog/2025/9/1/how-to-use-ai-to-accelerate-your-career-in-2025/" rel="alternate"/><updated>2025-09-01T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0d018b08-65a0-3c3d-85a6-959936d7f3fb</id><content type="html">&lt;p&gt;Everyone knows LLMs can help with coding and drafting emails. But there are less obvious ways to hack your career with AI that can save you hours and make you more effective at work.&lt;/p&gt;
&lt;p&gt;Here are 10 strategies I've tested, with sample prompts you can steal:&lt;/p&gt;
&lt;h2 id="draft-presentations-that-actually-land"&gt;Draft presentations that actually land&lt;/h2&gt;&lt;p&gt;Most people start with slides. Start with your audience instead.&lt;/p&gt;
&lt;p&gt;First, research who you're presenting to. If you know specific attendees, have ChatGPT or Claude build dossiers from their public profiles - LinkedIn, company bios, recent interviews. Then ask the LLM what these people care about most.&lt;/p&gt;
&lt;p&gt;Next, have it craft your core message and angle based on those audience insights. Finally, get it to describe in words how each slide should look before you build anything - making sure to feed in both your audience research and your refined message. This approach works because you're designing for actual humans, not abstract concepts.&lt;/p&gt;
&lt;p&gt;Pro tip: Or just skip the manual work entirely and use Gamma.ai.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I'm presenting to &lt;code&gt;[specific people/roles]&lt;/code&gt;. Here are their LinkedIn profiles: &lt;code&gt;[paste]&lt;/code&gt;. What do they care about most professionally right now?&lt;/p&gt;
&lt;p&gt;My presentation topic is &lt;code&gt;[topic]&lt;/code&gt;. My audience cares about &lt;code&gt;[insights from above]&lt;/code&gt;. Help me craft a compelling angle that will resonate with them.&lt;/p&gt;
&lt;p&gt;Generate slide-by-slide instructions for a &lt;code&gt;[number]&lt;/code&gt;-slide presentation on &lt;code&gt;[topic]&lt;/code&gt;. My audience is &lt;code&gt;[audience description]&lt;/code&gt; and they care about &lt;code&gt;[audience insights from research]&lt;/code&gt;. My core message is &lt;code&gt;[refined message/angle]&lt;/code&gt;. For each slide, tell me: the title (which should be the main point of that slide), what elements to include, and how to lay them out. The title style should be &lt;code&gt;[describe your preferred title style - e.g., "a clear statement that makes the key point, not just a topic heading"]&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="research-your-negotiation-counterparts-like-a-detective"&gt;Research your negotiation counterparts like a detective&lt;/h2&gt;&lt;p&gt;Context is everything in negotiations. Feed your LLM everything you know about the other party - their backgrounds, the situation they're in, potential constraints they're facing.&lt;/p&gt;
&lt;p&gt;Describe your own circumstances, goals, and BATNA (Best Alternative to a Negotiated Agreement). Then iterate with the LLM on potential objections and counter-strategies.&lt;/p&gt;
&lt;p&gt;The more specific information you provide, the better it gets at uncovering blind spots you hadn't considered.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I'm negotiating &lt;code&gt;[situation]&lt;/code&gt; with &lt;code&gt;[specific people/roles]&lt;/code&gt;. Here's what I know about them: &lt;code&gt;[paste background]&lt;/code&gt;. Here are my goals: &lt;code&gt;[paste goals]&lt;/code&gt;. My BATNA is: &lt;code&gt;[paste alternative]&lt;/code&gt;. What objections might they raise?&lt;/p&gt;
&lt;p&gt;Given this context: &lt;code&gt;[paste situation details]&lt;/code&gt;, what leverage points might I have that I'm not seeing?&lt;/p&gt;
&lt;p&gt;If I propose &lt;code&gt;[specific ask]&lt;/code&gt;, how might they respond based on &lt;code&gt;[paste their constraints/motivations]&lt;/code&gt;? Help me prepare counter-responses.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="transform-content-between-formats-effortlessly"&gt;Transform content between formats effortlessly&lt;/h2&gt;&lt;p&gt;You wrote a technical document that needs to become a blog post. Or you have a blog post that needs to become a slide deck. Instead of starting from scratch, let the LLM remix your existing content into the right format.&lt;/p&gt;
&lt;p&gt;The key is being specific about your target audience. A technical document transformed for executives needs different language and emphasis than one transformed for peer engineers. Without clear audience context, the LLM can't make effective choices about tone, depth, and focus.&lt;/p&gt;
&lt;p&gt;This works for any content transformation - meeting notes to executive summaries, brainstorming sessions to project proposals, quarterly reviews to team updates. Just remember: same content, different audience, different approach.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Transform this technical document into a blog post for &lt;code&gt;[specific people/roles - describe their background, interests, and level of technical knowledge]&lt;/code&gt;: &lt;code&gt;[paste content]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Turn these meeting notes into an executive summary for &lt;code&gt;[specific people/roles - include their role, priorities, and what they care about]&lt;/code&gt;: &lt;code&gt;[paste notes]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Convert this brainstorming session into a structured project proposal for &lt;code&gt;[specific people/roles - describe their concerns and what convinces them]&lt;/code&gt;: &lt;code&gt;[paste ideas]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="fill-out-administrative-forms-without-the-dread"&gt;Fill out administrative forms without the dread&lt;/h2&gt;&lt;p&gt;OKRs, performance reviews, expense reports - we all have forms that feel like bureaucratic hurdles. Here's the hack: don't write directly into the form.&lt;/p&gt;
&lt;p&gt;Instead, do a brain dump by talking through your accomplishments and goals. Transcribe this (voice memos work great), then paste the form questions plus your transcript into ChatGPT. Have it fill out the form for you, then copy-paste back.&lt;/p&gt;
&lt;p&gt;What used to take half a day now takes 30 minutes.&lt;/p&gt;
&lt;p&gt;Check out the Dia browser, which lets you insert LLM-generated text directly into web forms.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Here are my form questions: &lt;code&gt;[paste]&lt;/code&gt;. Here's my brain dump of accomplishments: &lt;code&gt;[paste transcript]&lt;/code&gt;. Fill out the form professionally.&lt;/p&gt;
&lt;p&gt;Help me write OKRs based on this verbal dump of my goals: &lt;code&gt;[paste transcript]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Turn this expense description into proper business justification: &lt;code&gt;[paste description]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="ghost-write-in-your-own-voice"&gt;Ghost-write in your own voice&lt;/h2&gt;&lt;p&gt;Including this blog post, I start by verbally dumping my ideas into a Markdown file in Obsidian, usually via voice transcription. Then I have Claude ghostwrite using my tone - my verbal dump contains my natural writing patterns, plus I feed it samples of my previous blog posts.
The key is multiple editing rounds. I push hard on the LLM during edits, which is how I make the content truly mine. I don't publish anything until it's been through at least two rounds of refinement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Here's my verbal brain dump: &lt;code&gt;[paste]&lt;/code&gt;. Here are samples of my writing style: &lt;code&gt;[paste examples]&lt;/code&gt;. Ghostwrite my brain dump as a blog post in my voice.&lt;/p&gt;
&lt;p&gt;This draft doesn't sound like me yet. Make it more &lt;code&gt;[specific style notes]&lt;/code&gt;. Here's what my natural voice sounds like: &lt;code&gt;[paste examples]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Polish this draft but keep my conversational tone and specific phrases: &lt;code&gt;[paste draft]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="prepare-manager-updates-that-actually-help-your-career"&gt;Prepare manager updates that actually help your career&lt;/h2&gt;&lt;p&gt;Use the same strategy as ghostwriting, but tailor it to your manager's level and scope. You want your manager to have the details they need to advocate for you effectively.&lt;/p&gt;
&lt;p&gt;Most managers don't have time to critique your use of AI - they just need to stay informed. Keep a running log in a shared space, ideally structured like "updates, problems, questions" organized by project.&lt;/p&gt;
&lt;p&gt;As both an employee and a manager, I can tell you: teammates who spoon-feed structured weekly updates are gold. It shapes your manager's memory of your contributions and honestly makes my job as a manager easier because it lets me focus on coaching and strategic support rather than hunting for status updates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Turn this brain dump into a structured manager update: &lt;code&gt;[paste notes]&lt;/code&gt;. Format as Updates/Problems/Questions by project.&lt;/p&gt;
&lt;p&gt;My manager is &lt;code&gt;[specific people/roles - description of their role/priorities]&lt;/code&gt;. Here's what I accomplished this week: &lt;code&gt;[paste list]&lt;/code&gt;. Write an update that helps them advocate for me.&lt;/p&gt;
&lt;p&gt;Summarize my quarterly achievements in a way that highlights impact and aligns with &lt;code&gt;[paste company priorities]&lt;/code&gt;: &lt;code&gt;[paste accomplishments]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="personalize-communications-with-important-people"&gt;Personalize communications with important people&lt;/h2&gt;&lt;p&gt;Use research mode to build context on VIPs you're meeting. Feed that research into your communication planning. This works because our digital footprints reveal what we care about, and LLMs are trained on massive examples of human interaction.&lt;/p&gt;
&lt;p&gt;The result: messages that land because they're tailored to what actually matters to the recipient.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I'm reaching out to &lt;code&gt;[specific people/roles]&lt;/code&gt; about &lt;code&gt;[topic]&lt;/code&gt;. Here's their background: &lt;code&gt;[paste research]&lt;/code&gt;. Help me craft a personalized message that will resonate.&lt;/p&gt;
&lt;p&gt;Based on this person's recent posts/interviews: &lt;code&gt;[paste]&lt;/code&gt;, what communication style and topics should I focus on?&lt;/p&gt;
&lt;p&gt;I need to follow up on &lt;code&gt;[situation]&lt;/code&gt; with &lt;code&gt;[specific people/roles who have these characteristics]&lt;/code&gt;. Write a message that acknowledges &lt;code&gt;[paste their priorities/constraints]&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="make-seamless-edits-to-any-document"&gt;Make seamless edits to any document&lt;/h2&gt;&lt;p&gt;When you have a long piece of writing (code, essays, reports), don't manually hunt for where to make changes. Just dictate your edits into an LLM chat.&lt;/p&gt;
&lt;p&gt;Add this to your system prompt: "When you make edits that I request, please make them seamless with the rest of the context."
This prevents the LLM from injecting walls of text and instead makes surgical, contextual changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In this document: &lt;code&gt;[paste]&lt;/code&gt;, I need to add information about &lt;code&gt;[topic]&lt;/code&gt; in the section about &lt;code&gt;[section]&lt;/code&gt;. Make it seamless with the existing content.&lt;/p&gt;
&lt;p&gt;Change the tone of this section from &lt;code&gt;[current tone]&lt;/code&gt; to &lt;code&gt;[desired tone]&lt;/code&gt; without changing the key points: &lt;code&gt;[paste section]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This paragraph needs to be more concise while keeping the main message: &lt;code&gt;[paste paragraph]&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="organize-your-accomplishments-by-competency"&gt;Organize your accomplishments by competency&lt;/h2&gt;&lt;p&gt;Dump all your achievements into a voice memo and have an LLM organize them by your company's competency framework. Most performance reviews follow standard patterns - leadership, technical skills, collaboration.&lt;/p&gt;
&lt;p&gt;LLMs excel at categorizing your work and suggesting which examples best demonstrate each competency. No more staring at blank forms trying to remember what you did six months ago.&lt;/p&gt;
&lt;p&gt;This is essentially automating the &lt;a href="https://www.youtube.com/shorts/gbkv8Asadh0"&gt;brag doc that Steve Huynh recommends&lt;/a&gt; - but with AI doing the heavy lifting of organization and categorization. I've written more about &lt;a href="https://ericmjl.github.io/blog/2024/2/29/your-first-90-days-at-work-what-should-you-do/"&gt;building your accomplishments record in your first 90 days&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Here are my accomplishments from this year: &lt;code&gt;[paste list]&lt;/code&gt;. Organize them by these competencies: &lt;code&gt;[paste framework]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I need examples that demonstrate leadership. Here's everything I've done: &lt;code&gt;[paste]&lt;/code&gt;. Which examples best show leadership impact?&lt;/p&gt;
&lt;p&gt;Help me identify gaps in my competency examples. Here's what I have: &lt;code&gt;[paste organized list]&lt;/code&gt;. What areas need stronger examples?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="practice-difficult-conversations-before-they-happen"&gt;Practice difficult conversations before they happen&lt;/h2&gt;&lt;p&gt;Feed in everything you know about the person you need to talk to - their communication style, recent stressors, past reactions, what motivates them. Have the LLM help you craft the right tone and timing, then practice by having it roleplay as them.&lt;/p&gt;
&lt;p&gt;Difficult conversations often fail not because of what you say, but how and when you say it. This prep work is like having a rehearsal before the real performance.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=yMOmmnjy3sE"&gt;Jeremy Utley demonstrates this technique&lt;/a&gt; of using AI to roleplay difficult conversations before they happen.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I need to have a difficult conversation with &lt;code&gt;[specific people/roles]&lt;/code&gt; about &lt;code&gt;[topic]&lt;/code&gt;. Here's their communication style: &lt;code&gt;[paste description]&lt;/code&gt;. Here's the situation: &lt;code&gt;[paste context]&lt;/code&gt;. Help me plan my approach.&lt;/p&gt;
&lt;p&gt;Roleplay as &lt;code&gt;[specific people/roles with these characteristics]&lt;/code&gt; while I practice this conversation about &lt;code&gt;[topic]&lt;/code&gt;. Push back as they would based on &lt;code&gt;[paste their known concerns]&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I want to ask for &lt;code&gt;[specific request]&lt;/code&gt;. This person typically responds to &lt;code&gt;[paste motivation style]&lt;/code&gt;. How should I frame this conversation?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="force-ai-to-challenge-your-assumptions"&gt;Force AI to challenge your assumptions&lt;/h2&gt;&lt;p&gt;Don't just use LLMs to confirm what you already think. Actively ask them to disagree with you and surface blind spots. This is especially powerful for strategic decisions, project planning, or career moves where you might be too close to see potential problems.&lt;/p&gt;
&lt;p&gt;The key is being explicit about wanting pushback. LLMs are trained to be helpful and agreeable, so you need to specifically request criticism and alternative perspectives.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sample prompts:&lt;/strong&gt;&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I'm planning to &lt;code&gt;[paste decision/strategy]&lt;/code&gt;. Play devil's advocate - what are the strongest arguments against this approach?&lt;/p&gt;
&lt;p&gt;Challenge my assumptions about &lt;code&gt;[situation]&lt;/code&gt;. Here's how I see it: &lt;code&gt;[paste your perspective]&lt;/code&gt;. What am I missing or getting wrong?&lt;/p&gt;
&lt;p&gt;I think &lt;code&gt;[paste opinion/plan]&lt;/code&gt;. Give me three reasons why someone smart might disagree with me, and explain their reasoning.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-principles-that-make-this-work"&gt;The principles that make this work&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Know your audience first.&lt;/strong&gt; Whether you're transforming content, crafting presentations, or writing updates for your manager, everything starts with understanding who you're communicating with. LLMs can't make effective choices about tone, depth, and focus without clear audience context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Research beats assumptions.&lt;/strong&gt; Don't guess what people want or how they'll react. Feed LLMs specific information about negotiation counterparts, presentation audiences, or conversation partners. The more context you provide, the better the output.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Speak, don't type.&lt;/strong&gt; Voice transcription captures your natural patterns and is faster than typing. Use it for brain dumps, accomplishment reviews, and initial drafts.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automate the tedious, elevate the strategic.&lt;/strong&gt; Let LLMs handle forms, formatting, and content transformation so you can focus on relationships, creative problem-solving, and high-level strategy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practice before it matters.&lt;/strong&gt; Use LLMs to rehearse difficult conversations, anticipate objections in negotiations, and stress-test your thinking before real situations.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Seek challenge, not just confirmation.&lt;/strong&gt; Explicitly ask LLMs to disagree with you, surface blind spots, and present counterarguments. This prevents echo chambers and sharpens your thinking.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Document everything for future you.&lt;/strong&gt; Keep accomplishment records, conversation insights, and successful prompts organized. Your future self will thank you during performance reviews and job transitions.&lt;/p&gt;
&lt;p&gt;The real power isn't in the AI doing your thinking for you - it's in using AI to handle the mechanics so you can focus on the strategy, relationships, and creative problem-solving that actually advance your career.&lt;/p&gt;
&lt;h2 id="ai-is-a-mirror"&gt;AI is a mirror&lt;/h2&gt;&lt;p&gt;As Jeremy Utley puts it: "AI is a mirror." If we want our brains to rot, we can use AI to make our brains rot. Or we can use AI to sharpen how we're thinking, be more effective and efficient at how we're working.&lt;/p&gt;
&lt;p&gt;The choice is yours. Use these techniques to elevate your career, not replace your judgment.&lt;/p&gt;
</content></entry><entry><title>How to communicate with lab scientists (when you're the data person)</title><link href="https://ericmjl.github.io/blog/2025/8/24/how-to-communicate-with-lab-scientists-when-youre-the-data-person/" rel="alternate"/><updated>2025-08-24T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:49bf5cd1-1578-32ca-9e45-4a16a6ece092</id><content type="html">&lt;p&gt;Imagine this scenario: A data scientist explains a hierarchical Bayesian model for 45 minutes. Beautiful math. Elegant handling of batch effects. The lab scientists are polite but glazed over. Finally, someone interrupts: "Sorry, but should we move this compound forward or not?"&lt;/p&gt;
&lt;p&gt;The data scientist hadn't even calculated that probability.&lt;/p&gt;
&lt;p&gt;Sound familiar?&lt;/p&gt;
&lt;p&gt;If you're a statistician or data scientist in biotech, you've probably been there. You've spent hours on sophisticated analyses, crafted beautiful slides about your methods, and watched your audience's eyes glaze over while you explained mixed-effects models.&lt;/p&gt;
&lt;p&gt;Meanwhile, they just needed to know if they should spend $200K on the next experiment.&lt;/p&gt;
&lt;p&gt;Here's the thing: Lab scientists aren't struggling to understand your statistics because they're not smart enough. They're brilliant experts who've spent years mastering protein folding, cell signaling, or synthetic chemistry. They're just juggling their own complex problems and need you to translate your analysis into something they can act on.&lt;/p&gt;
&lt;p&gt;Today, I'm going to show you exactly how to do that.&lt;/p&gt;
&lt;h2 id="here-s-what-we-re-covering"&gt;Here's what we're covering:&lt;/h2&gt;&lt;ol&gt;
&lt;li&gt;Your communication budget is finite — spend it wisely&lt;/li&gt;
&lt;li&gt;Know what mode they're in before you open your laptop&lt;/li&gt;
&lt;li&gt;Use the three-layer translation model&lt;/li&gt;
&lt;li&gt;Decode what they're really asking&lt;/li&gt;
&lt;li&gt;Build trust through clarity, not complexity&lt;/li&gt;
&lt;li&gt;Master the decision-first meeting structure&lt;/li&gt;
&lt;li&gt;Ask yourself what they'll ask you&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="1-your-communication-budget-is-finite-spend-it-wisely"&gt;1. Your communication budget is finite — spend it wisely&lt;/h2&gt;&lt;p&gt;Every interaction has a finite "communication budget" — limited attention, time, and cognitive load. Most data scientists spend this budget like tourists with foreign currency, not realizing the exchange rate.&lt;/p&gt;
&lt;p&gt;Think about your last presentation. Where did you spend your time?&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;The typical (failed) allocation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;60% on methodology and statistical details&lt;/li&gt;
&lt;li&gt;30% on results (tables, coefficients, credible intervals)&lt;/li&gt;
&lt;li&gt;10% on "what this means" (usually rushed at the end)&lt;/li&gt;
&lt;li&gt;0% on "what you should do next"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I get it. We're trained to show our work. We think rigor equals value. We assume that if we explain our methods thoroughly enough, scientists will understand what to do.&lt;/p&gt;
&lt;p&gt;But here's what actually works:&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;The allocation that drives decisions:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;10% on methods (just enough for credibility)&lt;/li&gt;
&lt;li&gt;20% on results (simplified, visual, contextual)&lt;/li&gt;
&lt;li&gt;40% on implications for their specific decisions&lt;/li&gt;
&lt;li&gt;30% on uncertainty and what it means for their next steps&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;But context matters. A curious scientist with time might genuinely want 30% methods—they're building mental models for future decisions. Someone facing a go/no-go decision tomorrow? They need 70% decision implications, minimal methods.&lt;/p&gt;
&lt;p&gt;The key is adopting &lt;a href="https://en.wikipedia.org/wiki/BLUF_(communication"&gt;BLUF (Bottom-Line Up-Front)&lt;/a&gt;). Structure your presentation by working backwards from the decision to be made.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt; Start with the decision and recommendation, then work backwards to the evidence that supports it. Lead with "Based on our analysis, I recommend we proceed with Compound A because there's an 87% probability it meets our potency threshold."&lt;/p&gt;
&lt;p&gt;This tells them immediately what they need to know, then you can spend the remaining time explaining why.&lt;/p&gt;
&lt;p&gt;Here's what happens when you don't use BLUF: A data scientist spent an entire program review meeting walking through their elegant approach to handling missing data. Really sophisticated stuff. Multiple imputation with careful consideration of the missing-at-random assumption.&lt;/p&gt;
&lt;p&gt;Twenty minutes in, the program lead interrupted: "This is interesting, but we need to decide today whether to advance this molecule. Does it meet our potency threshold or not?"&lt;/p&gt;
&lt;p&gt;They hadn't even calculated that probability. They'd spent their entire communication budget on something that wasn't even the program lead's concern that day.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;With BLUF, they would have started:&lt;/strong&gt; "Based on our analysis, I recommend we advance this molecule. There's an 82% probability it meets our potency threshold, even accounting for the missing data. Here's how I handled the missing data to arrive at this conclusion..."&lt;/p&gt;
&lt;h2 id="2-know-what-mode-they-re-in-before-you-open-your-laptop"&gt;2. Know what mode they're in before you open your laptop&lt;/h2&gt;&lt;p&gt;Lab scientists operate in three distinct modes, and each requires a completely different communication approach.&lt;/p&gt;
&lt;h3 id="decision-mode-most-of-the-time"&gt;Decision Mode (Most of the time)&lt;/h3&gt;&lt;p&gt;They're under time pressure for go/no-go decisions. Maybe it's a pipeline review tomorrow. Maybe they need to order materials today. Maybe the synthesis team is literally waiting for their answer.&lt;/p&gt;
&lt;p&gt;Signs you'll hear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"What's the bottom line?"&lt;/li&gt;
&lt;li&gt;"Should we proceed?"&lt;/li&gt;
&lt;li&gt;"Just tell me if it worked"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What they need: Probability of success and a clear recommendation. That's it.&lt;/p&gt;
&lt;h3 id="learning-mode-when-they-have-bandwidth"&gt;Learning Mode (When they have bandwidth)&lt;/h3&gt;&lt;p&gt;They're genuinely curious about your methods. Maybe they're trying to understand why this analysis differs from last time. Maybe they're building intuition for future experiments.&lt;/p&gt;
&lt;p&gt;Signs you'll hear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"How does that work?"&lt;/li&gt;
&lt;li&gt;"Why did you choose that approach?"&lt;/li&gt;
&lt;li&gt;"Can you explain the intuition behind this?"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What they need: Mental models and intuition, not mathematical formulas.&lt;/p&gt;
&lt;h3 id="validation-mode-testing-if-they-can-trust-you"&gt;Validation Mode (Testing if they can trust you)&lt;/h3&gt;&lt;p&gt;They're not really interested in learning—they're assessing whether they can rely on your judgment for million-dollar decisions.&lt;/p&gt;
&lt;p&gt;Signs you'll hear:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"What assumptions did you make?"&lt;/li&gt;
&lt;li&gt;"How does this handle batch effects?"&lt;/li&gt;
&lt;li&gt;"What if the data is wrong?"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What they need: Confidence that you've been rigorous without the full mathematical proof.&lt;/p&gt;
&lt;p&gt;Here's the mistake most of us make:&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;Wrong approach:&lt;/strong&gt; Launch into methods explanation regardless of mode&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Right approach:&lt;/strong&gt; Start with the decision and recommendation, then adapt the explanation depth based on their mode&lt;/p&gt;
&lt;p&gt;Most data scientists default to teaching mode when scientists are in decision mode. That's like giving someone a recipe when they just asked if dinner's ready.&lt;/p&gt;
&lt;p&gt;Consider this scenario: A scientist approaches a data scientist with dose-response data. The data scientist starts explaining their Bayesian approach to EC50 estimation. Five minutes in, the scientist stops them: "I just need to know if this is more potent than our current lead."&lt;/p&gt;
&lt;p&gt;She was in Decision Mode. The data scientist was in Teaching Mode. Complete mismatch.&lt;/p&gt;
&lt;p&gt;The better approach is to be deliberate rather than reactive. Before any meeting, clarify the goals upfront. Ask what they're trying to decide. Talk to stakeholders beforehand to understand the context. Do the pre-work rather than trying to read body language in real-time.&lt;/p&gt;
&lt;h2 id="3-use-the-three-layer-translation-model"&gt;3. Use the three-layer translation model&lt;/h2&gt;&lt;p&gt;You think in distributions. They think in decisions. This gap is why brilliant analyses often fail to drive action.&lt;/p&gt;
&lt;p&gt;Here's the framework that works for bridging that gap:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 1: Statistical Reality (Keep this in your head)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Full posterior distributions&lt;/li&gt;
&lt;li&gt;Model assumptions&lt;/li&gt;
&lt;li&gt;Fancy math&lt;/li&gt;
&lt;li&gt;All the technical details you love&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This layer is for you. It ensures your analysis is rigorous. But it stays in your head or the appendix.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 2: Scientific Meaning (The bridge)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What the analysis means for their biological hypothesis&lt;/li&gt;
&lt;li&gt;How the statistics relate to their experimental design&lt;/li&gt;
&lt;li&gt;The full richness of uncertainty&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's the key: Keep the full distribution at this layer. Don't collapse to point estimates yet. You're translating statistics to science, but you're not making decisions yet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Layer 3: Decision Layer (What they actually need)&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;NOW integrate over posteriors for specific probabilities&lt;/li&gt;
&lt;li&gt;"There's an 87% chance this compound beats your TPP threshold"&lt;/li&gt;
&lt;li&gt;"With 90% probability, this is your best compound"&lt;/li&gt;
&lt;li&gt;"You need 12 more samples to reach 95% confidence"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The magic is waiting until the last possible moment to collapse distributions into decision probabilities. Why? Because different decisions need different integrations of the same posterior.&lt;/p&gt;
&lt;p&gt;Let me show you what I mean:&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;Wrong (Layer 1 bleeding into communication):&lt;/strong&gt;
"The posterior distribution for the treatment effect has a 95% credible interval of [0.15, 0.31] with a mean of 0.23."&lt;/p&gt;
&lt;p&gt;What does a lab scientist do with this? Nothing. It's statistical reality without translation.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Right (Layer 3, decision-focused):&lt;/strong&gt;
"There's a 92% probability your treatment exceeds the TPP threshold. If you need 95% confidence for the program milestone, run 20 more samples. If 90% is acceptable for an early read, you can proceed now."&lt;/p&gt;
&lt;p&gt;See the difference? One is statistical reporting. The other enables a decision.&lt;/p&gt;
&lt;p&gt;The same posterior distribution might need to answer multiple questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;"What's the probability this exceeds our TPP?" (integrate above threshold)&lt;/li&gt;
&lt;li&gt;"What's the probability this is our best compound?" (compare posteriors)&lt;/li&gt;
&lt;li&gt;"How many samples until 95% confidence?" (project forward)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By keeping the full distribution until Layer 3, you can answer whatever decision question they actually have, not the one you assumed they had.&lt;/p&gt;
&lt;h2 id="4-decode-what-they-re-really-asking"&gt;4. Decode what they're really asking&lt;/h2&gt;&lt;p&gt;Scientists may ask statistics questions when they mean decision questions. Learning to translate is a superpower.&lt;/p&gt;
&lt;p&gt;Here's your decoder ring:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"Is this significant?"&lt;/strong&gt; They're not asking about p-values. They're asking: "Should I continue this line of research?"&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"What's the confidence?"&lt;/strong&gt; They don't want credible intervals. They're asking: "How wrong could this decision be?"&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"Did it work?"&lt;/strong&gt; They don't care about effect sizes. They're asking: "Is the effect large enough to matter for my application?"&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"Can you check the stats?"&lt;/strong&gt; They don't want a methods seminar. They're asking: "I need ammunition for my go/no-go meeting tomorrow."&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;"How robust is this?"&lt;/strong&gt; They're not necessarily interested in sensitivity analyses. They're asking: "Can I trust this decision?"&lt;/p&gt;
&lt;p&gt;Every lab scientist in biotech faces the same five decisions over and over:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Resource allocation:&lt;/strong&gt; Should I invest more time/money/FTEs in this direction?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipeline progression:&lt;/strong&gt; Is this ready for the next stage?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experimental design:&lt;/strong&gt; Should I modify my approach or repeat?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Program decisions:&lt;/strong&gt; Continue, pivot, or kill?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform decisions:&lt;/strong&gt; Is this assay/method worth scaling up?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your job isn't to answer their literal question. It's to figure out which of these five decisions they're really trying to make.&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;Wrong:&lt;/strong&gt; Answer the literal statistics question they asked&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Right:&lt;/strong&gt; Answer the decision they're trying to make&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Try this:&lt;/strong&gt; When first asked to partner on an analysis, ask: "What decision are you trying to make with this data?"&lt;/p&gt;
&lt;p&gt;Then frame everything around that decision.&lt;/p&gt;
&lt;p&gt;Here's a common scenario: A scientist asks the data team to "check if the groups are different." The data scientist could run their standard analysis and report "statistically significant difference detected." Technically correct. Completely useless.&lt;/p&gt;
&lt;p&gt;Instead, imagine asking: "What decision does this inform?"&lt;/p&gt;
&lt;p&gt;Turns out, they need to know if the new formulation is at least 20% better than the current one — otherwise, it wasn't worth the reformulation costs. The groups were statistically different, but only by 8%. The real answer was: "Don't reformulate."&lt;/p&gt;
&lt;p&gt;That's the difference between answering questions and enabling decisions.&lt;/p&gt;
&lt;h2 id="5-build-trust-through-clarity-not-complexity"&gt;5. Build trust through clarity, not complexity&lt;/h2&gt;&lt;p&gt;Here's the paradox: Most data scientists think trust comes from showing their work.&lt;/p&gt;
&lt;p&gt;This is more nuanced than you might think.&lt;/p&gt;
&lt;p&gt;Over-explaining methods actually reduces trust because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It signals insecurity about your results&lt;/li&gt;
&lt;li&gt;It wastes precious communication budget&lt;/li&gt;
&lt;li&gt;It feels like gatekeeping with jargon&lt;/li&gt;
&lt;li&gt;It suggests you don't understand what they need&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What actually builds trust:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Leading with clear probabilities for their go/no-go decisions&lt;/li&gt;
&lt;li&gt;Showing how probability changes with more data&lt;/li&gt;
&lt;li&gt;Being precise about uncertainty without hedging&lt;/li&gt;
&lt;li&gt;Speaking their language, not yours&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;🚫 &lt;strong&gt;Trust-killing:&lt;/strong&gt; "Well, it depends on your assumptions about the prior, and if we consider the hierarchical structure of the random effects, controlling for batch-to-batch variation, we can say that under certain conditions..."&lt;/p&gt;
&lt;p&gt;This sounds like you're not confident in your answer.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Trust-building:&lt;/strong&gt; "There's an 89% chance this works. If you need 95% confidence before scaling up, test 3 more concentrations. Here's why I'm confident in that number: I've accounted for batch effects, and even in the worst-case scenario, you're still above 82%."&lt;/p&gt;
&lt;p&gt;Clear. Actionable. Confident.&lt;/p&gt;
&lt;p&gt;The beauty of probabilistic thinking here: "We're 78% confident" is infinitely clearer than "statistically significant." You can directly answer: "What's the probability we're making the wrong decision?"&lt;/p&gt;
&lt;p&gt;That's a question every scientist understands.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The methods appendix approach:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When you do need to establish technical credibility, try this structure:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;One slide of method basics (just enough to show rigor)&lt;/li&gt;
&lt;li&gt;Key assumptions in plain English (Pro tip: AI tools can help translate technical assumptions into audience-appropriate language)&lt;/li&gt;
&lt;li&gt;Details available but not forced&lt;/li&gt;
&lt;li&gt;For the genuinely curious: "Happy to dive into the model after we nail down your decision"&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Smart data scientists keep a technical appendix for every analysis. It has all the details they're proud of—the clever missing data handling, the hierarchical structure, the prior specifications.&lt;/p&gt;
&lt;p&gt;But they only show it when asked. And here's what happens: People trust them more because they respect everyone's time enough not to force it on them.&lt;/p&gt;
&lt;h2 id="6-master-the-decision-first-meeting-structure"&gt;6. Master the decision-first meeting structure&lt;/h2&gt;&lt;p&gt;Stop opening with methods. Stop it right now.&lt;/p&gt;
&lt;p&gt;Start with their decision.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The structure that works:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;State the decision context upfront&lt;/strong&gt;: "We're here to discuss [specific decision]. Here's what the data tells us."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Give the probability and recommendation immediately&lt;/strong&gt;: "There's an 89% probability of success. I recommend proceeding."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Show how probability changes with more data&lt;/strong&gt;: "With 10 more samples, we'd get to 95% confidence."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Discuss what could change your assessment&lt;/strong&gt;: "This assumes your batch effects stay consistent."&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offer details only if requested&lt;/strong&gt;: "Want me to walk through how I got there?"&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let me show you the difference:&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;Wrong meeting flow:&lt;/strong&gt;
"Thanks for coming. So I started by examining the data structure, and I noticed some heteroscedasticity in the residuals, which suggested we might need a more complex variance structure. I tried several approaches, including a Box-Cox transformation, but ultimately settled on a hierarchical model because... [20 minutes later]... so in conclusion, it might work."&lt;/p&gt;
&lt;p&gt;By the time you get to the conclusion, they've stopped listening.&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Right meeting flow:&lt;/strong&gt;
"We're here to discuss whether to advance Compound X to synthesis. Based on your assay data, there's an 89% probability this compound exceeds your 10nM potency requirement. I recommend proceeding to synthesis scale-up. If you need 95% confidence instead of 89%, I'd recommend testing 3 more concentrations first. Want me to walk through how I got there?"&lt;/p&gt;
&lt;p&gt;Notice how the second version:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Answers their question immediately&lt;/li&gt;
&lt;li&gt;Gives them options based on risk tolerance&lt;/li&gt;
&lt;li&gt;Respects their time&lt;/li&gt;
&lt;li&gt;Offers details rather than forcing them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;The email version:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;Subject: Compound X: 89% probability of meeting TPP

Hi Sarah,

**Decision:** Compound X has an 89% probability of meeting your 10nM potency requirement. Recommend proceeding to synthesis.

**Key evidence:**
- Consistent effect across all three batches
- Dose-response curve shows clear relationship
- Even worst-case scenario keeps you above 15nM

**Next steps:** If you need &amp;gt;95% confidence, test 3 additional concentrations. Otherwise, proceed with synthesis.

Technical details in attached appendix if interested.

Best,
[Your name]
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's it. Decision first. Evidence second. Details optional.&lt;/p&gt;
&lt;h2 id="7-speak-their-language-literally"&gt;7. Speak their language (literally)&lt;/h2&gt;&lt;p&gt;Here's what most data scientists miss: You need to understand their domain as deeply as they do.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The measurement method matters more than your statistical method.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;A scientist tells you they're using ELISA to measure protein levels. You nod and proceed with your analysis. But did you ask:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What's the detection limit?&lt;/li&gt;
&lt;li&gt;How does the antibody specificity affect your readout?&lt;/li&gt;
&lt;li&gt;Are there known cross-reactivities that could confound your results?&lt;/li&gt;
&lt;li&gt;What's the coefficient of variation across replicates?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These aren't &lt;em&gt;merely&lt;/em&gt; statistical questions — they're also biological questions that determine whether your analysis is even valid.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Be deeply curious about their methods.&lt;/strong&gt; Ask about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The specific assay they're using and its limitations&lt;/li&gt;
&lt;li&gt;How they handle sample preparation and storage&lt;/li&gt;
&lt;li&gt;What controls they're running and why&lt;/li&gt;
&lt;li&gt;The historical performance of this measurement in their hands&lt;/li&gt;
&lt;li&gt;What could go wrong and how they'd know&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Learn their terminology.&lt;/strong&gt; Don't just understand what they're measuring, but understand how they think about it. When they say "potency," do they mean EC50, IC50, or something else? When they talk about "efficacy," are they referring to maximal response, potency, or both?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick domain mastery checklist:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What are the three most common failure modes for this assay?&lt;/li&gt;
&lt;li&gt;What does "good" look like in their world?&lt;/li&gt;
&lt;li&gt;What would make them suspicious of the data?&lt;/li&gt;
&lt;li&gt;How do they typically handle outliers or unexpected results?&lt;/li&gt;
&lt;li&gt;What's the gold standard measurement they're comparing against?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: A data scientist was analyzing dose-response data from a cell-based assay. The scientist mentioned they were using a "luminescence readout." The data scientist asked about the detection range, learned it was $10^3$ to $10^6$ RLU, and immediately spotted that their highest concentration was saturating the detector. The analysis would have been meaningless without understanding that technical limitation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The payoff:&lt;/strong&gt; When you speak their language, you don't just communicate better, you also analyze better. You spot confounders they might miss. You suggest controls they haven't thought of. You become a true collaborator, not just a service provider.&lt;/p&gt;
&lt;h2 id="8-ask-yourself-what-they-ll-ask-you"&gt;8. Ask yourself what they'll ask you&lt;/h2&gt;&lt;p&gt;Every scientist has patterns. Learn them.&lt;/p&gt;
&lt;p&gt;Your PI always asks about sample size? Pre-calculate the probability of detecting meaningful effects.
Your biomarker lead obsesses over false positives? Lead with the posterior probability of true effects.
The chemistry team cares about synthesis feasibility? Include yield probabilities from your Bayesian model.&lt;/p&gt;
&lt;p&gt;This isn't mind-reading. It's paying attention.&lt;/p&gt;
&lt;p&gt;🚫 &lt;strong&gt;Reactive approach:&lt;/strong&gt;
Wait for their questions, scramble for answers, promise to "get back to you on that"&lt;/p&gt;
&lt;p&gt;✅ &lt;strong&gt;Proactive approach:&lt;/strong&gt;
"I know you usually want to know about batch effects, so I checked—they're negligible. Here's how I verified..."&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pattern matching checklist:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;What did they ask in the last three meetings?&lt;/li&gt;
&lt;li&gt;What decisions do they usually struggle with?&lt;/li&gt;
&lt;li&gt;What makes them nervous about moving forward?&lt;/li&gt;
&lt;li&gt;What would convince them this is real?&lt;/li&gt;
&lt;li&gt;What got them in trouble before?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Example: One program lead always asks: "What if we're wrong?" Every. Single. Time.&lt;/p&gt;
&lt;p&gt;The smartest data scientists now anticipate this and always include: "If we're wrong about this, here's what we'd see in the next experiment. Here's our bail-out plan. Here's the cost of being wrong versus the cost of being slow."&lt;/p&gt;
&lt;p&gt;She doesn't ask anymore. She trusts that they've thought it through.&lt;/p&gt;
&lt;p&gt;Another scientist always wants to know if we have enough evidence. So the prepared data scientist leads with: "There's an 85% probability that the treatment effect exceeds your minimum meaningful difference."&lt;/p&gt;
&lt;p&gt;Pre-answering questions isn't just efficient—it builds massive trust. It shows you understand their concerns and you're thinking ahead. Trust me, &lt;strong&gt;this is a career hack!&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="the-bottom-line"&gt;The bottom line&lt;/h2&gt;&lt;p&gt;Most data scientists in biotech spend 80% of their communication budget on methods that their collaborators—brilliant scientists juggling their own complex problems—don't have bandwidth to process.&lt;/p&gt;
&lt;p&gt;You're doing the equivalent of giving someone a recipe when they just asked if dinner's ready.&lt;/p&gt;
&lt;p&gt;The shift is simple but not easy: Stop defaulting to education mode. Start asking "What decision are you trying to make?" Then translate your sophisticated analysis into the probability they need to make that decision.&lt;/p&gt;
&lt;p&gt;This isn't about dumbing down your work. It's about translating between two expert domains—like a diplomat translating between heads of state. The lab scientists you work with have spent years mastering complex biological systems. They need translation, not education.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your action items:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;When first asked for analysis:&lt;/strong&gt; Start with "What decision are you trying to make with this data?" Don't begin any analysis until you know.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review your last presentation:&lt;/strong&gt; Did you lead with the decision (BLUF) or bury it in methods? If methods came first, restructure.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practice probability statements:&lt;/strong&gt; Instead of showing credible intervals, say "There's an X% probability that..." It's clearer and more actionable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learn their measurement methods:&lt;/strong&gt; Ask about detection limits, controls, and failure modes before analyzing their data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build a pattern map:&lt;/strong&gt; Write down what each of your regular collaborators usually asks. Answer it proactively next time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Create a technical appendix:&lt;/strong&gt; Put all your beautiful methods somewhere. Just don't force people to sit through it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The best data scientists are the ones whose collaborators make the best decisions.&lt;/p&gt;
&lt;p&gt;And that starts with spending your communication budget on what actually matters to the people you're trying to help.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;What patterns have you noticed in your collaborations? What questions do your scientists always ask? Let me know; I'd love to hear what's working (or not working) for you!&lt;/em&gt;&lt;/p&gt;
</content></entry><entry><title>Wicked Python trickery - dynamically patch a Python function's source code at runtime</title><link href="https://ericmjl.github.io/blog/2025/8/23/wicked-python-trickery-dynamically-patch-a-python-functions-source-code-at-runtime/" rel="alternate"/><updated>2025-08-23T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:3ced3d47-73a9-378e-aead-d8279180cc9f</id><content type="html">&lt;p&gt;So today, I learned a very dangerous and yet fascinating trick.&lt;/p&gt;
&lt;p&gt;It's possible to dynamically change a Python function's source code &lt;em&gt;at runtime&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;What this does is open a world of possibilities in building AI bots!&lt;/p&gt;
&lt;h2 id="how-this-actually-works"&gt;How this actually works&lt;/h2&gt;&lt;p&gt;Every function has a &lt;code&gt;.__code__&lt;/code&gt; attribute. For example, for this function:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;something&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;something.__code__&lt;/code&gt; looks like this:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="n"&gt;something&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="mh"&gt;0x149bdfc90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;/var/folders/36/vb250n_s0zncstw3sk74qfxr0000gn/T/marimo_80086/__marimo__cell_kJqw_.py&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If I were to execute &lt;code&gt;something()&lt;/code&gt;, it would return a &lt;code&gt;NotImplementedError&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now, let's say that, for some reason that I shall not speculate, I decided that I wanted &lt;code&gt;something()&lt;/code&gt; to instead do multiplication by 2. I can create new source code:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;new_code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;def something(x: int) -&amp;gt; int:&lt;/span&gt;
&lt;span class="s2"&gt;    return x * 2&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I can do the following three magical steps to swap it in.&lt;/p&gt;
&lt;p&gt;Firstly, compile the code into bytecode:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;compiled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;new_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;lt;magic&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;exec&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The three arguments to &lt;code&gt;compile&lt;/code&gt; are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The code to compile (&lt;code&gt;new_code&lt;/code&gt;),&lt;/li&gt;
&lt;li&gt;The filename in which the code is compiled (&lt;code&gt;&amp;lt;magic&amp;gt;&lt;/code&gt;), and&lt;/li&gt;
&lt;li&gt;The mode in which compilation happens (in this case, &lt;code&gt;exec&lt;/code&gt; mode).&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;On the third point, the docstring of &lt;code&gt;compile&lt;/code&gt; explains what the three modes are:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The mode must be 'exec' to compile a module, 'single' to compile a single (interactive) statement, or 'eval' to compile an expression.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The &lt;code&gt;compiled&lt;/code&gt; object now is a "code object":&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="mh"&gt;0x149bcbad0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;lt;magic&amp;gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I can then execute the compiled code to make it imported into a particular namespace.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;ns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;compiled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, the three arguments passed to &lt;code&gt;exec&lt;/code&gt; are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The code we want to execute (&lt;code&gt;compiled&lt;/code&gt;), and in this case, by "executing" it after being compiled in &lt;code&gt;exec&lt;/code&gt; mode, we are really just simulating an &lt;code&gt;import&lt;/code&gt; into our namespace.&lt;/li&gt;
&lt;li&gt;The globals (&lt;code&gt;{}&lt;/code&gt;), which in this case are passed in as an empty dictionary. These are the global variables that are available to the function at runtime.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ns&lt;/code&gt; is the "namespace" in which we want the function to be present; namespaces in Python are just dictionary mappings from function/object name to the function/object itself.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Finally, I can replace my existing function with the compiled function inserted into the &lt;code&gt;ns&lt;/code&gt; namespace:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;something_new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ns&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;something&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;something_new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;# this will print 42 to stdout!&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But really, the real lesson here is not that one can monkeypatch over an existing Python function's source code at runtime, but that you can actually &lt;strong&gt;compile the string of a Python function definition and give it access to a namespace's variables&lt;/strong&gt;, including that of the current global namespace.&lt;/p&gt;
&lt;h2 id="when-would-you-ever-want-to-do-this"&gt;When would you ever want to do this?&lt;/h2&gt;&lt;p&gt;At first glance, never really! This is a bit of hackery that lives on the fringes of Python-land, and is basically a party trick.&lt;/p&gt;
&lt;p&gt;But as it turns out, I &lt;em&gt;actually&lt;/em&gt; had a real motivation for wanting to do this.&lt;/p&gt;
&lt;p&gt;Within &lt;a href="https://github.com/ericmjl/llamabot"&gt;LlamaBot&lt;/a&gt;, I've always had &lt;code&gt;AgentBot&lt;/code&gt; as a first-pass implementation of what I think an LLM agent should look like, having studied LLM agent implementations in other libraries. However, I've never been fully satisfied with &lt;code&gt;AgentBot&lt;/code&gt;'s implementation. The core issue was that it mixed too many concerns together - function execution, function call determination, and user response generation all lived in the same loop.&lt;/p&gt;
&lt;p&gt;Here's what &lt;code&gt;AgentBot&lt;/code&gt; looked like at a high level:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;AgentBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleBot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_iterations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_iterations&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# get response object, passing in messages&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="c1"&gt;# Execute tool calls if they are present&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tool_call&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tool_calls&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name_to_tools&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool_call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="c1"&gt;# continue until LLM decides we&amp;#39;re done.&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="c1"&gt;# just respond to users.&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While this worked, it wasn't great at separating concerns. I had function execution mixed in with function call determination mixed in with responding to a user.&lt;/p&gt;
&lt;p&gt;The bigger limitation was with code execution tools. My original implementation isolated generated code in a Docker container sandbox, which was secure but meant the code couldn't access variables from my current Python runtime. This severely limited what kinds of useful tasks the bot could perform with my existing data and variables.&lt;/p&gt;
&lt;p&gt;I realized that if I could:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Use an LLM to generate Python functions that referenced existing variables in my runtime,&lt;/li&gt;
&lt;li&gt;Compile those functions on-the-fly within the same Python environment, and&lt;/li&gt;
&lt;li&gt;Execute them with access to my current namespace,&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I could build something much more powerful. This led me to create &lt;code&gt;ToolBot&lt;/code&gt; within LlamaBot.&lt;/p&gt;
&lt;h2 id="toolbot-focuses-on-tool-selection-instead-of-execution"&gt;ToolBot focuses on tool selection instead of execution&lt;/h2&gt;&lt;p&gt;&lt;code&gt;ToolBot&lt;/code&gt; takes a different approach - it focuses purely on tool selection rather than execution. Here's the key structure:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ToolBot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SimpleBot&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Initialize with core tools like today_date and respond_to_user&lt;/span&gt;
        &lt;span class="n"&gt;all_tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;today_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;respond_to_user&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;all_tools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json_schema&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_tools&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name_to_tool_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_tools&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Process message and return tool calls (but don&amp;#39;t execute them)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;tool_calls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_tool_calls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tool_calls&lt;/span&gt;  &lt;span class="c1"&gt;# Just return the calls, don&amp;#39;t execute&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The key insight: &lt;code&gt;ToolBot&lt;/code&gt; just selects a tool to be executed, but does &lt;em&gt;not&lt;/em&gt; execute it. Instead, it returns the tools to be called to the external environment, giving you full control over execution.&lt;/p&gt;
&lt;h2 id="the-magic-happens-with-write-and-execute-code"&gt;The magic happens with write_and_execute_code&lt;/h2&gt;&lt;p&gt;One of the most powerful tools that can be chosen is &lt;code&gt;write_and_execute_code&lt;/code&gt;. Here's the core implementation:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_and_execute_code&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;globals_dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@tool&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_and_execute_code_wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placeholder_function&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword_args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;Write and execute `placeholder_function` with the passed in `keyword_args`.&lt;/span&gt;

&lt;span class="s2"&gt;        Use this tool for any task that requires custom Python code generation and execution.&lt;/span&gt;
&lt;span class="s2"&gt;        This tool has access to ALL globals in the current runtime environment (variables, dataframes, functions, etc.).&lt;/span&gt;
&lt;span class="s2"&gt;        Perfect for: data analysis, calculations, transformations, visualizations, custom algorithms.&lt;/span&gt;

&lt;span class="s2"&gt;        ## Code Generation Guidelines:&lt;/span&gt;

&lt;span class="s2"&gt;        1. **Write self-contained Python functions** with ALL imports inside the function body&lt;/span&gt;
&lt;span class="s2"&gt;        2. **Place all imports at the beginning of the function**: import statements must be the first lines inside the function&lt;/span&gt;
&lt;span class="s2"&gt;        3. **Include all required libraries**: pandas, numpy, matplotlib, etc. - import everything the function needs&lt;/span&gt;
&lt;span class="s2"&gt;        4. **Leverage existing global variables**: Can reference variables that exist in the runtime&lt;/span&gt;
&lt;span class="s2"&gt;        5. **Include proper error handling** and docstrings&lt;/span&gt;
&lt;span class="s2"&gt;        6. **Provide keyword arguments** when the function requires parameters&lt;/span&gt;
&lt;span class="s2"&gt;        7. **Make functions reusable** - they will be stored globally for future use&lt;/span&gt;
&lt;span class="s2"&gt;        8. **ALWAYS RETURN A VALUE**: Every function must explicitly return something - never just print, display, or show results without returning them. Even for plotting functions, return the figure/axes object.&lt;/span&gt;

&lt;span class="s2"&gt;        ## Function Arguments Handling:&lt;/span&gt;

&lt;span class="s2"&gt;        **CRITICAL**: You MUST match the function signature with the keyword_args:&lt;/span&gt;
&lt;span class="s2"&gt;        - **If your function takes NO parameters** (e.g., `def analyze_data():`), then pass an **empty dictionary**: `&lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;
&lt;span class="s2"&gt;        - **If your function takes parameters** (e.g., `def filter_data(min_age, department):`), then pass the required arguments as a dictionary: `{&amp;quot;min_age&amp;quot;: 30, &amp;quot;department&amp;quot;: &amp;quot;Engineering&amp;quot;}`&lt;/span&gt;
&lt;span class="s2"&gt;        - **Never pass keyword_args that don&amp;#39;t match the function signature** - this will cause execution errors&lt;/span&gt;

&lt;span class="s2"&gt;        ## Code Structure Example:&lt;/span&gt;

&lt;span class="s2"&gt;        ```python&lt;/span&gt;
&lt;span class="s2"&gt;        # Function with NO parameters - use empty dict &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;
&lt;span class="s2"&gt;        def analyze_departments():&lt;/span&gt;
&lt;span class="s2"&gt;            &amp;#39;&amp;#39;&amp;#39;Analyze department performance.&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="s2"&gt;            import pandas as pd&lt;/span&gt;
&lt;span class="s2"&gt;            import numpy as np&lt;/span&gt;
&lt;span class="s2"&gt;            result = fake_df.groupby(&amp;#39;department&amp;#39;)[&amp;#39;salary&amp;#39;].mean()&lt;/span&gt;
&lt;span class="s2"&gt;            return result&lt;/span&gt;
&lt;span class="s2"&gt;        # Function WITH parameters - pass matching keyword_args&lt;/span&gt;
&lt;span class="s2"&gt;        def filter_employees(min_age, department):&lt;/span&gt;
&lt;span class="s2"&gt;            &amp;#39;&amp;#39;&amp;#39;Filter employees by criteria.&amp;#39;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="s2"&gt;            import pandas as pd&lt;/span&gt;
&lt;span class="s2"&gt;            filtered = fake_df[(fake_df[&amp;#39;age&amp;#39;] &amp;gt;= min_age) &amp;amp; (fake_df[&amp;#39;department&amp;#39;] == department)]&lt;/span&gt;
&lt;span class="s2"&gt;            return filtered&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;pre&gt;&lt;code&gt;    ## Return Value Requirements:

    - **Data analysis functions**: Return the computed results (numbers, DataFrames, lists, dictionaries)
    - **Plotting functions**: Return the figure or axes object (e.g., `return fig` or `return plt.gca()`)
    - **Filter/transformation functions**: Return the processed data
    - **Calculation functions**: Return the calculated values
    - **Utility functions**: Return relevant output (status, processed data, etc.)
    - **Never return None implicitly** - always have an explicit return statement

    ## Code Access Capabilities:

    The generated code will have access to:
    - All global variables and dataframes in the current session
    - Any previously defined functions
    - The ability to import any standard Python libraries within the function
    - The ability to create new reusable functions that will be stored globally
    :param placeholder_function: The function to execute (complete Python function as string).
    :param keyword_args: The keyword arguments to pass to the function (dictionary matching function parameters).
    :return: The result of the function execution.
    """

    # Parse the code to extract the function name
    tree = ast.parse(placeholder_function)
    function_name = None
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            function_name = node.name
            break
    # Compile and execute the function with access to globals
    ns = globals_dict
    compiled = compile(placeholder_function, "&amp;lt;llm&amp;gt;", "exec")
    exec(compiled, globals_dict, ns)
    return ns[function_name](**keyword_args)

return write_and_execute_code_wrapper
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;
This extensive docstring gets passed as part of the JSON schema and effectively serves as instructions to the LLM on when and how to use this tool. I stripped out logging and error handling to simplify what's shown here, but the actual codebase has more robustness built in.

Notice how `ToolBot`, and more specifically `write_and_execute_code`, gains explicit access to the `globals()` dictionary when a user passes it in. This approach allows us to ensure that function execution takes place within the proper namespace. If `ToolBot` chooses `write_and_execute_code`, I can control exactly where and how it executes within my Python runtime environment - and this opens up a world of possibilities!

For example, inspired by [the Marimo blog](https://marimo.io/blog/marimo-chat), which wrote about generative UIs and tool calling:

&amp;gt; marimo’s chat interface supports Generative UI - the ability to stream rich, interactive UI components directly from LLM responses. This goes beyond traditional text and markdown outputs, allowing chatbots to return dynamic elements like tables, charts, and interactive visualizations.

I decided to build out a _generalized_ version of a tool that an LLM could choose to call on that would also have access to any variable present within the runtime environment... much like Marimo's AI chat has access to any variable within the environment with an `@variable_name`, now I just dump the full set of `globals()` into the LLM's context window, and that's what `write_and_execute_code` looked like.

Here's an example, imagine I have two dataframes that I want an LLM to manipulate. Without `write_and_execute_code`, I'd have to write bespoke tools for the dataframe, in which I access the `df` as a "global" variable, much like the following:

```python
@lmb.tool
def chart_data(x_encoding: str, y_encoding: str, color: str):
    """Generate an altair chart"""
    import altair as alt
    return (
        alt.Chart(df)
        .mark_circle()
        .encode(x=x_encoding, y=y_encoding, color=color)
        .properties(width=500)
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So the writing on the wall is that I'd have to write one tool for every possible operation that I'd desire, but that's a big hassle. With this &lt;code&gt;globals()&lt;/code&gt;, &lt;code&gt;compile&lt;/code&gt;, and &lt;code&gt;exec&lt;/code&gt; trickery baked into &lt;code&gt;write_and_execute_code&lt;/code&gt;, I no longer have to specify bespoke tools for the environment that I'm in!&lt;/p&gt;
&lt;p&gt;Further more, inspired by the Marimo blog post, &lt;code&gt;ToolBot&lt;/code&gt; is designed to just do the tool picking, delegating the execution and return of the broader LLM-powered Python program back to the developer. In this way, I can give myself more flexibility when building entire "Agentic" programs, more so than if I were to use &lt;code&gt;AgentBot&lt;/code&gt; in its current form. It allowed me to build a more powerful version of a tool-calling agent using &lt;code&gt;ToolBot&lt;/code&gt; with generative UIs in a Marimo notebook. For this, it's easier to demo via a screencast instead of by me describing it in prose:&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/tk5wvb556f8?si=sXSRulZ2ooBplapr" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;p&gt;And if you're curious to try running it, you can run it with the following command:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;https://raw.githubusercontent.com/ericmjl/website/refs/heads/main/content/blog/wicked-python-trickery-dynamically-patch-a-python-functions-source-code-at-runtime/agents.py
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="security-concerns-are-very-real-with-this-approach"&gt;Security concerns are very real with this approach&lt;/h2&gt;&lt;p&gt;Comparing this to what we had before with &lt;code&gt;write_and_execute_script&lt;/code&gt;, which performed execution in a sandboxed Docker container with limited read/write capabilities, &lt;code&gt;write_and_execute_code&lt;/code&gt; is &lt;em&gt;much, much less secure&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Obviously, I'm playing with fire here. A malicious LLM output could run code directly and do enormous damage to my machine and from my machine to the outside world. I have yet to implement code sanitization, but one big idea I have, which I just learned through discourse with GPT-4, is to use &lt;a href="https://github.com/zopefoundation/RestrictedPython"&gt;Restricted Python&lt;/a&gt;. I think that will be the next big upgrade after I let the current version of &lt;code&gt;write_and_execute_code&lt;/code&gt; sit for a while.&lt;/p&gt;
&lt;p&gt;As such, I don't suggest that the &lt;code&gt;write_and_execute_code&lt;/code&gt; pattern be used for anything really serious in its current form.&lt;/p&gt;
&lt;h2 id="what-i-learned-from-this-python-trickery"&gt;What I learned from this Python trickery&lt;/h2&gt;&lt;p&gt;This journey taught me several things. First, Python's runtime is far more malleable than I initially realized - the ability to compile strings into executable code and inject them into specific namespaces opens up incredible possibilities for dynamic programming.&lt;/p&gt;
&lt;p&gt;Second, building effective LLM agents isn't just about the AI - it's about thoughtful system design. Separating tool selection from execution (as &lt;code&gt;ToolBot&lt;/code&gt; does) creates much more flexible and controllable systems than monolithic agents.&lt;/p&gt;
&lt;p&gt;Finally, this wouldn't have been possible without &lt;a href="https://ericmjl.github.io/blog/2025/6/7/principles-for-using-ai-autodidactically/"&gt;autodidactic learning with LLMs&lt;/a&gt;. I'm becoming more and more convinced that LLMs are a great tool for learning, but one must learn how to use them for learning, and one must &lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;earn the automation&lt;/a&gt; as well.&lt;/p&gt;
</content></entry><entry><title>Data scientists aren't becoming obsolete in the LLM era</title><link href="https://ericmjl.github.io/blog/2025/8/15/data-scientists-arent-becoming-obsolete-in-the-llm-era/" rel="alternate"/><updated>2025-08-15T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:8aed9fe5-d6f7-3703-9c7f-efb092c73a5d</id><content type="html">&lt;p&gt;I keep hearing the same question: "Are data scientists becoming obsolete now that LLMs can code?"&lt;/p&gt;
&lt;p&gt;The anxiety is understandable. When you watch Claude or ChatGPT write Python scripts, build models, and even debug code, it's natural to wonder where that leaves us. But here's what I've found after spending months integrating LLMs into my own workflow: they're not replacing us. They're fundamentally reshaping what it means to be a data scientist.&lt;/p&gt;
&lt;p&gt;To ponder this question properly, I examine it from two angles.&lt;/p&gt;
&lt;h2 id="how-are-llms-enhancing-our-existing-work"&gt;How are LLMs enhancing our existing work?&lt;/h2&gt;&lt;p&gt;The first angle is using LLMs as tools for data scientists. This means finding ways to incorporate them into our day-to-day work as consumers of LLM-powered applications.&lt;/p&gt;
&lt;p&gt;I've experienced the productivity-enhancing benefits firsthand. GitHub Copilot and Cursor have dramatically accelerated my coding. Research agents like Elicit.org help me navigate literature in ways that would have taken hours before. I use transcription tools to type faster than I can touch type by hand, getting my thoughts out of my brain closer to the speed at which I'm actually thinking. I rely on AI for cleaning up messy thoughts and as a thinking tool to help me draw out what I'm really trying to articulate.&lt;/p&gt;
&lt;p&gt;Having lived with these tools for months now, I think being proficient with AI-assisted coding is table stakes.&lt;/p&gt;
&lt;p&gt;Just as spreadsheets changed what we expected from accountants, AI assistance is now a baseline expectation. But there's a crucial skill here that goes beyond just using the tools: knowing how to use AI to verify information and catch the inevitable errors these systems make.&lt;/p&gt;
&lt;p&gt;More importantly, this is just the beginning.&lt;/p&gt;
&lt;h2 id="how-are-we-building-custom-llm-solutions"&gt;How are we building custom LLM solutions?&lt;/h2&gt;&lt;p&gt;The second angle is more profound: data scientists becoming part of the team that builds custom LLM agent workflows to accelerate others' work.&lt;/p&gt;
&lt;p&gt;Here's what this looks like in practice: You get hands-dirty with business workflows. You co-create with business partners to build new tools and ways of working that remove boring work from their plates. You build technical prototypes that prove out value, then partner with engineers for custom app builds where appropriate.&lt;/p&gt;
&lt;p&gt;The &lt;em&gt;scientist&lt;/em&gt; skill becomes crucial here: experimentation. You're figuring out whether a thing is actually working by measuring performance of LLM-based workflows and tying it back to business value. This is fundamentally different from being an app developer, a machine learning engineer, or a business analyst doing reporting and dashboards.
Those aren't really data science roles. The scientist in data science lies in hypothesizing, defining metrics and estimates, then testing and measuring them.&lt;/p&gt;
&lt;h2 id="what-does-the-scientist-in-data-scientist-mean-in-the-llm-era"&gt;What does the 'scientist' in 'data scientist' mean in the LLM era?&lt;/h2&gt;&lt;p&gt;Taking Hamel Husain and Shreya Shankar's course on LLM evaluation crystallized this for me. I'm much more convinced that the role of a data scientist is to measure, evaluate, and design metrics. It's going back to the science.&lt;/p&gt;
&lt;p&gt;Think about the parallel here. In discovery science, data scientists work with laboratory scientists and statisticians to hypothesize about relationships between molecular structure and biological activity, then together define what estimate we need to measure the performance of biological or chemical systems. They build machine learning models to predict those estimands from sequence and structure, test the hypotheses, and measure whether they hold. The estimands matter because they connect to whether a drug works or a process is optimized.&lt;/p&gt;
&lt;p&gt;With LLM applications automating business processes, it's analogous but the stakes are operational performance. You hypothesize that a particular LLM workflow will improve efficiency or accuracy. You define evaluation metrics—the equivalent of the assays you measure in lab science. You design experiments to test whether your hypothesis about the LLM's impact is correct. You build automation around measurement to continuously validate whether your hypotheses about improved workflows are actually playing out.&lt;/p&gt;
&lt;p&gt;In both contexts, you hypothesize, define, test, and measure. &lt;strong&gt;That's what a scientist does!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In what I'd describe as a meta move, data scientists should absolutely be experimenting with LLMs to create LLM-based tooling for their own work. We're uniquely positioned to understand both the technical possibilities and the measurement challenges these systems present.&lt;/p&gt;
&lt;h2 id="why-this-matters-more-than-ever"&gt;Why this matters more than ever&lt;/h2&gt;&lt;p&gt;This role differs fundamentally from what people might think we should become. We're not primarily app developers (that should be for software developers), even if we might ship and app or two out of necessity. We're not machine learning engineers building complex production pipelines (though we should be able to ship components that get stitched together on platforms). We're not business analysts doing reporting and dashboards, even if we do build visualizations to help with communication.&lt;/p&gt;
&lt;p&gt;Rather, we're scientists who hypothesize, define metrics, design estimates, test our ideas, and measure whether things work.&lt;/p&gt;
&lt;p&gt;Instead of making data scientists obsolete, the LLM era is returning us to our scientific roots while giving us incredibly powerful tools to work with. We're becoming builders of measurement systems that work at the intersection of business value and statistical rigor.&lt;/p&gt;
&lt;p&gt;I'd strongly encourage you to try both angles: become proficient with LLM tools for your daily work, and start experimenting with building custom LLM workflows for your organization. The beauty of this approach is that you're amplifying your ability to hypothesize what might work, define what matters, and measure whether it's actually working.&lt;/p&gt;
</content></entry><entry><title>Stop guessing at priors: R2D2's automated approach to Bayesian modeling</title><link href="https://ericmjl.github.io/blog/2025/8/6/stop-guessing-at-priors-r2d2s-automated-approach-to-bayesian-modeling/" rel="alternate"/><updated>2025-08-06T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:1426f8db-c4c6-3abf-8ecd-7449e42852d4</id><content type="html">&lt;p&gt;When I first encountered the R2D2 (R²-induced Dirichlet Decomposition) framework (Zhang et al., 2020), I was struck by its intuitive approach to Bayesian regularization. Instead of placing priors on individual regression coefficients and hoping for the best, R2D2 lets you directly specify your beliefs about how much variance the model should explain. But what really fascinated me was how the framework elegantly extends from simple linear regression to complex multilevel models through a series of principled modifications.&lt;/p&gt;
&lt;p&gt;This post documents my journey understanding the progression from the basic R2D2 shrinkage prior to its sophisticated multilevel variant (R2D2M2), with stops along the way to explore generalized linear models. What emerged was a beautiful mathematical architecture where each extension builds naturally on the previous.&lt;/p&gt;
&lt;h2 id="the-foundation-r2d2-shrinkage-prior"&gt;The foundation: R2D2 shrinkage prior&lt;/h2&gt;&lt;p&gt;The journey begins with the elegant insight that motivated the original R2D2 framework: why not place a prior directly on the coefficient of determination (R²) rather than fumbling with individual coefficient priors? The challenge with individual coefficient priors isn't just knowing where to center them, but defining appropriate variance parameters - it's remarkably difficult to know a priori how much variability each coefficient should have.&lt;/p&gt;
&lt;h3 id="the-core-mathematical-insight"&gt;The core mathematical insight&lt;/h3&gt;&lt;p&gt;For any model, R² represents the proportion of output variance that can be explained:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R² = explained variance / total variance = W / (W + σ²)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Rearranging this relationship shows us what W actually represents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R² = W / (W + σ²)
R²(W + σ²) = W
R²W + R²σ² = W
R²σ² = W - R²W = W(1 - R²)
W = σ² * (R² / (1 - R²))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This reveals that W is the &lt;strong&gt;total explained variance&lt;/strong&gt; (on the data scale), which equals the signal-to-noise ratio multiplied by the noise variance. Let's define the signal-to-noise ratio as:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;τ² = R² / (1 - R²)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;So that:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;W = σ² * τ²
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This gives us:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;τ² = 1&lt;/strong&gt;: Signal equals noise (R² = 0.5)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;τ² = 4&lt;/strong&gt;: Signal is 4 times stronger than noise (R² = 0.8)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;τ² = 0.25&lt;/strong&gt;: Noise is 4 times stronger than signal (R² = 0.2)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The R2D2 framework starts by placing a Beta prior on R²:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;r_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;tau_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tau_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Zhang et al. show that when R² has a Beta(a,b) prior, the induced prior density for τ² = R²/(1-R²) follows a Beta Prime distribution BP(a,b), giving us intuitive control over model fit through the familiar Beta hyperparameters.&lt;/p&gt;
&lt;h3 id="allocating-explained-variance-the-dirichlet-decomposition"&gt;Allocating explained variance: the Dirichlet decomposition&lt;/h3&gt;&lt;p&gt;But here's where R2D2 gets clever. Instead of requiring the modeler to manually specify variance parameters for each predictor's prior, it uses a &lt;strong&gt;Dirichlet decomposition&lt;/strong&gt; to automatically allocate the total explained variance W across predictors:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# W is the total explained variance to allocate&lt;/span&gt;
&lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dirichlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;phi&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a_pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;predictors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;lambda_j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lambda_j&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;predictors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means &lt;code&gt;φⱼ × W = λⱼ&lt;/code&gt; answers the question: &lt;em&gt;"What fraction of the total explained variance does predictor j get?"&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Example&lt;/strong&gt;: If τ² = 4 (signal is 4 times stronger than noise) and σ² = 2, then W = 8, and if φ = [0.5, 0.3, 0.2], then:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predictor 1: λ₁ = 0.5 × 8 = 4 (gets 50% of explained variance)&lt;/li&gt;
&lt;li&gt;Predictor 2: λ₂ = 0.3 × 8 = 2.4 (gets 30% of explained variance)&lt;/li&gt;
&lt;li&gt;Predictor 3: λ₃ = 0.2 × 8 = 1.6 (gets 20% of explained variance)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As Zhang et al. describe, this creates adaptive behavior where "the heavy tail reduces the bias in estimation of large coefficients, while the high concentration around zero shrinks the irrelevant coefficients heavily to zero, thus reducing the noise" - factors that explain a lot of the output variance get allocated more of the total explained variance (larger λⱼ values), while factors that don't explain much output variance get allocated less explained variance (smaller λⱼ values).&lt;/p&gt;
&lt;h3 id="the-r2d2-model"&gt;The R2D2 model&lt;/h3&gt;&lt;p&gt;Bringing these pieces together - the R² prior, the Dirichlet variance allocation, and the coefficient distributions - we get the R2D2 model:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Noise level (data scale)&lt;/span&gt;
    &lt;span class="n"&gt;sigma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HalfNormal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sigma&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# R² prior (intuitive model fit control)&lt;/span&gt;
    &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;r_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Signal-to-noise ratio&lt;/span&gt;
    &lt;span class="n"&gt;tau_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tau_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Total explained variance&lt;/span&gt;
    &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;W&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Local variance allocation (competitive)&lt;/span&gt;
    &lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dirichlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;phi&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a_pi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;predictors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;lambda_j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;lambda_j&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;W&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;predictors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Coefficients with allocated variance&lt;/span&gt;
    &lt;span class="n"&gt;scale_j&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sigma&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lambda_j&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# For Laplace priors&lt;/span&gt;
    &lt;span class="n"&gt;beta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Laplace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;beta&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scale_j&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;predictors&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Standard linear likelihood&lt;/span&gt;
    &lt;span class="n"&gt;mu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;likelihood&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;y&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;obs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The beauty of this approach lies in the competitive nature of the Dirichlet allocation: all predictors compete for the total explained variance W. If one predictor becomes more important (higher φⱼ), others must become less important. This creates natural sparsity and prevents overfitting. The signal-to-noise ratio τ² provides intuitive control over model complexity, while W gives us the actual variance scale for coefficient priors.&lt;/p&gt;
&lt;h2 id="first-extension-r2d2-for-generalized-linear-models"&gt;First extension: R2D2 for generalized linear models&lt;/h2&gt;&lt;p&gt;The first major challenge came when extending R2D2 to non-Gaussian outcomes. Yanchenko et al. (2021) tackled this problem by developing clever approximation methods that preserve the intuitive R² interpretation. The beautiful relationship &lt;code&gt;R² = W/(W+σ²)&lt;/code&gt; that made everything work cleanly suddenly becomes complex when dealing with Poisson counts, binary outcomes, or other GLM families.&lt;/p&gt;
&lt;h3 id="the-challenge-no-more-simple-s2"&gt;The challenge: no more simple σ²&lt;/h3&gt;&lt;p&gt;In GLMs, the "noise" isn't a simple σ² anymore. Instead, we have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Poisson&lt;/strong&gt;: Variance equals the mean (&lt;code&gt;σ²(η) = e^η&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Binomial&lt;/strong&gt;: Variance depends on probability (&lt;code&gt;σ²(η) = μ(η)[1-μ(η)]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gaussian&lt;/strong&gt;: Still simple (&lt;code&gt;σ²(η) = σ²&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This breaks our clean R² = W/(W+σ²) relationship because now both the signal and noise are functions of the linear predictor η.&lt;/p&gt;
&lt;h3 id="the-elegant-solution-linear-approximation"&gt;The elegant solution: linear approximation&lt;/h3&gt;&lt;p&gt;The GLM extension uses a brilliant linear approximation approach. As Yanchenko et al. describe, "applying a first-order Taylor series approximation of μ(η) and σ²(η) around β₀" allows them to handle the GLM complexity. We approximate the complex GLM relationship around the intercept β₀:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;R² ≈ W/(W + s²(β₀))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Where &lt;code&gt;s²(β₀) = σ²(β₀)/[μ'(β₀)]²&lt;/code&gt; is the "effective noise" for each GLM family:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gaussian&lt;/strong&gt;: &lt;code&gt;s²(β₀) = σ²&lt;/code&gt; (no change needed!)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Poisson&lt;/strong&gt;: &lt;code&gt;s²(β₀) = e^{-β₀}&lt;/code&gt; (depends on baseline rate)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Logistic&lt;/strong&gt;: &lt;code&gt;s²(β₀) = μ(β₀)(1-μ(β₀))/[μ'(β₀)]²&lt;/code&gt; (depends on baseline probability)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="what-this-achieves"&gt;What this achieves&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;The genius&lt;/strong&gt;: We keep all the interpretability and mathematical structure of the linear R2D2 case, but just compute a smarter "noise" term that respects the GLM family's variance structure.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Same intuitive R² prior!&lt;/span&gt;
    &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;r_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# GLM-specific &amp;quot;effective noise&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;poisson&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;s_sq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;s_sq&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;beta0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;family&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;binomial&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;exp_beta0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beta0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mu_beta0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exp_beta0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exp_beta0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mu_prime_beta0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exp_beta0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;exp_beta0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;s_sq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;s_sq&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu_beta0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mu_beta0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;power&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mu_prime_beta0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Same competitive allocation structure!&lt;/span&gt;
    &lt;span class="n"&gt;tau_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tau_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;W&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;W&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;s_sq&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dirichlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;phi&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;xi0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;components&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The elegance of this approach becomes clear when we step back and see what's happening conceptually. We're essentially asking "what would σ² be if this GLM were actually a linear model?" and using that as our effective noise term. This preserves all the intuitive benefits of R2D2 while handling GLM complexity. The signal-to-noise ratio τ² remains the same intuitive control parameter, while W adapts to the GLM's variance structure.&lt;/p&gt;
&lt;h2 id="the-great-leap-r2d2m2-for-multilevel-models"&gt;The great leap: R2D2M2 for multilevel models&lt;/h2&gt;&lt;p&gt;The most sophisticated extension addresses the challenge of multilevel models with multiple grouping factors - the kind of complex experimental designs common in laboratory research. Aguilar &amp;amp; Bürkner (2022) developed the R2D2M2 prior to handle this complexity while preserving the intuitive variance decomposition interpretation.&lt;/p&gt;
&lt;h3 id="the-multilevel-challenge"&gt;The multilevel challenge&lt;/h3&gt;&lt;p&gt;Consider a laboratory experiment with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Predictors&lt;/strong&gt;: Gene expression, Age, Treatment dose&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grouping factors&lt;/strong&gt;: Mouse ID, MicroRNA ID, Stress condition&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Traditional approaches assign independent priors to each effect:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Traditional (problematic) approach&lt;/span&gt;
&lt;span class="n"&gt;beta_gene&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;λ_gene&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;beta_age&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;λ_age&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;mouse_effects&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;λ_mouse&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;microRNA_effects&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;λ_microRNA&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;stress_effects&lt;/span&gt; &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;λ_stress&lt;/span&gt;&lt;span class="err"&gt;²&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;The problem&lt;/strong&gt;: As you add more predictors and grouping factors, the implied R² prior becomes increasingly concentrated near 1 (the maximum possible R² value). This happens because each additional effect adds its own independent variance contribution, causing the total expected explained variance to grow without bound, leading to overfitting-prone models that expect near-perfect fit a priori.&lt;/p&gt;
&lt;h3 id="the-r2d2m2-solution-type-level-variance-allocation"&gt;The R2D2M2 solution: type-level variance allocation&lt;/h3&gt;&lt;p&gt;The key insight from Aguilar &amp;amp; Bürkner is that R2D2M2 extends the Dirichlet decomposition to handle multiple &lt;strong&gt;types&lt;/strong&gt; of effects while preserving hierarchical pooling:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Component calculation for laboratory data&lt;/span&gt;
&lt;span class="n"&gt;n_components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n_predictors&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;n_grouping_factors&lt;/span&gt;
&lt;span class="n"&gt;n_components&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;  &lt;span class="c1"&gt;# gene_expr + age + dose + mouse + microRNA + stress&lt;/span&gt;

&lt;span class="n"&gt;component_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;population_gene_expr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# Population-level effects&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;population_age&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;population_dose&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mouse_intercepts&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# Group-specific intercept types&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;microRNA_intercepts&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;stress_intercepts&amp;#39;&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The key innovation here is subtle but powerful: instead of allocating variance to individual groups (Mouse 1, Mouse 2, etc.), we allocate variance to &lt;strong&gt;types&lt;/strong&gt; of effects. All mice share one variance prior, all microRNAs share another, etc.&lt;/p&gt;
&lt;h3 id="the-complete-r2d2m2-framework"&gt;The complete R2D2M2 framework&lt;/h3&gt;&lt;p&gt;Let's see how this all comes together in practice. The R2D2M2 model combines the R² prior, the extended Dirichlet allocation, and the hierarchical variance structure:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Same intuitive R² control&lt;/span&gt;
    &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Beta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;r_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;alpha_r2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;beta&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;beta_r2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tau_squared&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deterministic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;tau_squared&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;r_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Extended Dirichlet allocation across ALL effect types&lt;/span&gt;
    &lt;span class="n"&gt;phi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dirichlet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;phi&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;concentration&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;components&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Population-level effects - each gets its own φ component&lt;/span&gt;
    &lt;span class="n"&gt;beta_gene&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;beta_gene&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;beta_age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;beta_age&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;beta_dose&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;beta_dose&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                         &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Group-specific intercepts - each type gets its own φ component&lt;/span&gt;
    &lt;span class="n"&gt;mouse_intercepts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mouse_intercepts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mice&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;microRNA_intercepts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;microRNA_intercepts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                   &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;microRNAs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stress_intercepts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pm&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stress_intercepts&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                 &lt;span class="n"&gt;sigma&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;pt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sigma_squared&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;phi&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tau_squared&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                                 &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;stress_conditions&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Linear predictor combining all effects&lt;/span&gt;
    &lt;span class="n"&gt;eta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;beta_gene&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;gene_expr&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;beta_age&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;beta_dose&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dose&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="n"&gt;mouse_intercepts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mouse_ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="n"&gt;microRNA_intercepts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;microRNA_ids&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
           &lt;span class="n"&gt;stress_intercepts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;stress_conditions&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now that we've seen the mathematical structure, let's understand what makes this approach so effective.&lt;/p&gt;
&lt;h3 id="why-this-works-so-well"&gt;Why this works so well&lt;/h3&gt;&lt;p&gt;&lt;strong&gt;Hierarchical pooling preserved&lt;/strong&gt;: Individual mice still borrow strength from each other because they share the same variance component. Mouse A and Mouse B both use &lt;code&gt;mouse_scale&lt;/code&gt;, but have different intercept values.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Automatic factor importance&lt;/strong&gt;: The φ allocation tells you which experimental factors matter most. If φ = [0.15, 0.25, 0.05, 0.35, 0.15, 0.05], then mouse differences account for 35% of total explained variance - more than any single predictor!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Scalable complexity&lt;/strong&gt;: Works with any number of crossed or nested grouping factors without parameter explosion.&lt;/p&gt;
&lt;h2 id="the-unified-architecture"&gt;The unified architecture&lt;/h2&gt;&lt;p&gt;What strikes me most about this progression is how each extension elegantly handles new complexity:&lt;/p&gt;
&lt;p&gt;All three approaches maintain &lt;strong&gt;consistent R² control&lt;/strong&gt;, letting you directly specify beliefs about model fit through the same intuitive Beta prior on R². The competitive variance allocation through the Dirichlet mechanism creates healthy competition between effects across all approaches, preventing any single component from dominating. This leads to highly interpretable results - every approach produces φ components that directly tell you "what percentage of explained variance does each effect contribute?"&lt;/p&gt;
&lt;p&gt;The mathematical elegance is striking: each extension modifies just what needs to change. The GLM extension changes the noise term (σ² → s²(β₀)), while the M2 extension extends the allocation to multiple effect types. Finally, all approaches provide the same practical benefits - automatic shrinkage, sparsity induction, and protection against overfitting while maintaining computational tractability.&lt;/p&gt;
&lt;h2 id="when-to-use-what"&gt;When to use what&lt;/h2&gt;&lt;p&gt;Given these unified principles, how do you choose which approach fits your specific modeling scenario? Through this exploration, clear use cases emerged:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;When to Use&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R2D2 Shrinkage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Simple linear regression with multiple predictors, no grouping&lt;/td&gt;
&lt;td&gt;Gene expression ~ drug dose + age + weight&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R2D2 GLM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Non-Gaussian outcomes with simple structure&lt;/td&gt;
&lt;td&gt;Bacterial counts, binary outcomes, rate data&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;R2D2M2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Complex laboratory designs with multiple grouping factors (&lt;strong&gt;the laboratory default&lt;/strong&gt;)&lt;/td&gt;
&lt;td&gt;Laboratory experiments with mouse ID + microRNA ID + stress condition&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="looking-forward"&gt;Looking forward&lt;/h2&gt;&lt;p&gt;R2D2 solves a common frustration in Bayesian modeling: how do you set reasonable priors on dozens of coefficients without spending hours tweaking hyperparameters? Instead of guessing at individual coefficient priors, you specify one intuitive parameter - how much of the data variation you expect your model to explain - and R2D2 automatically figures out how to distribute that explanatory power across your predictors.&lt;/p&gt;
&lt;p&gt;For laboratory researchers especially, R2D2M2 delivers actionable scientific insight. When your model tells you that "mouse differences account for 35% of explained variance while stress conditions only account for 5%," you immediately know where to focus your experimental design efforts.&lt;/p&gt;
&lt;p&gt;This practical approach - starting with an intuitive question about model fit and letting the mathematics handle the details - shows how thoughtful statistical frameworks can make sophisticated modeling more accessible to working scientists. The PyMC library has implemented a modified form of R2D2M2 as the &lt;a href="https://www.pymc.io/projects/extras/en/stable/generated/pymc_extras.distributions.R2D2M2CP.html"&gt;&lt;code&gt;R2D2M2CP&lt;/code&gt; distribution&lt;/a&gt;, making these powerful priors readily available for practical use.&lt;/p&gt;
&lt;h2 id="references"&gt;References&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Zhang, Y. D., Naughton, B. P., Bondell, H. D., &amp;amp; Reich, B. J.&lt;/strong&gt; (2020). Bayesian Regression Using a Prior on the Model Fit: The R2-D2 Shrinkage Prior. &lt;em&gt;Journal of the American Statistical Association&lt;/em&gt;, 117(538), 862-874. &lt;a href="https://www.tandfonline.com/doi/full/10.1080/01621459.2020.1825449"&gt;https://www.tandfonline.com/doi/full/10.1080/01621459.2020.1825449&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Yanchenko, E., Bondell, H. D., &amp;amp; Reich, B. J.&lt;/strong&gt; (2021). The R2D2 Prior for Generalized Linear Mixed Models. &lt;em&gt;arXiv preprint arXiv:2111.10718&lt;/em&gt;. &lt;a href="https://arxiv.org/abs/2111.10718"&gt;https://arxiv.org/abs/2111.10718&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Aguilar, J. &amp;amp; Bürkner, P.&lt;/strong&gt; (2022). Intuitive Joint Priors for Bayesian Linear Multilevel Models: The R2D2M2 prior. &lt;em&gt;arXiv preprint arXiv:2208.07132&lt;/em&gt;. &lt;a href="https://arxiv.org/abs/2208.07132"&gt;https://arxiv.org/abs/2208.07132&lt;/a&gt;&lt;/p&gt;
</content></entry><entry><title>From nerd-sniped to shipped using AI as a thinking tool</title><link href="https://ericmjl.github.io/blog/2025/7/21/from-nerd-sniped-to-shipped-using-ai-as-a-thinking-tool/" rel="alternate"/><updated>2025-07-21T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:28012e93-0cdf-3534-aa24-409f9d057e10</id><content type="html">&lt;p&gt;What if I told you I shipped a complex feature rewrite in just two days using AI as a design partner?&lt;/p&gt;
&lt;p&gt;Before you roll your eyes at another "AI did everything for me" story, here's the catch: those two days were only possible because I spent months doing the hard work of earning that automation. Fresh off being thoroughly nerd-sniped by Joe Cheng (Posit PBC's CTO) at SciPy 2025, I found myself on a plane with a mission: finally implement robust graph-based memory for my Llamabot project.&lt;/p&gt;
&lt;p&gt;What happened next taught me everything about the difference between delegating thinking to AI versus using AI to amplify your thinking. The key insight? You have to earn your automation first.&lt;/p&gt;
&lt;h2 id="first-i-had-to-struggle-and-that-was-the-point"&gt;First, I had to struggle (and that was the point)&lt;/h2&gt;&lt;p&gt;The timeline is crucial to understanding why this approach worked. For four months, I'd been mulling over how graph-based memory for LLM applications could work. Then Joe read my Llamabot code (which at the time didn't have graph-based memory), we chatted, and I got completely nerd-sniped. Over the next few days, I decided I had to make graph memory happen, so I finally built a working prototype during my week in Seattle for work. (All on my personal laptop, keeping work and personal projects separate.)&lt;/p&gt;
&lt;p&gt;What made this experience transformative was having Joe look at my code. Here was someone I'd never met taking such a thorough look at my design choices - I was deeply impressed by how careful and thoughtful he was. That validation convinced me: it was time to do this right.&lt;/p&gt;
&lt;p&gt;But here's what mattered most: my prototype was fragile. Things were very intertwined with one another. Because everything was so coupled, I was naturally feeling the difficulty in making any changes. This hands-on struggle was teaching me exactly what needed to be separated and how to think about the architecture.&lt;/p&gt;
&lt;p&gt;This struggle wasn't wasted time - it was earning my automation.&lt;/p&gt;
&lt;h2 id="why-struggling-first-was-essential"&gt;Why struggling first was essential&lt;/h2&gt;&lt;p&gt;This connects directly to &lt;a href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/"&gt;an earlier blog post I wrote&lt;/a&gt; about earning your automation. I wouldn't have been able to critique AI the way I did if I hadn't first developed taste through hands-on struggle.&lt;/p&gt;
&lt;p&gt;That initial prototype work - building something fragile but functional by hand - gave me the judgment needed to meaningfully critique AI's suggestions. Without that foundation, I would have been delegating critical thinking to AI instead of using it as a thinking partner.&lt;/p&gt;
&lt;p&gt;The prototype taught me what worked, what didn't, and most importantly, what the real problems were that needed solving. When AI later proposed architectural changes, I could evaluate them against my lived experience of the pain points.&lt;/p&gt;
&lt;p&gt;This preparation set the stage for what happened next on that plane ride home.&lt;/p&gt;
&lt;h2 id="then-i-unleashed-ai-as-a-design-partner"&gt;Then I unleashed AI as a design partner&lt;/h2&gt;&lt;p&gt;At SEA-TAC airport with three hours until boarding, I decided this was it. Time to compress all my implementation work into a focused sprint. But instead of jumping straight into coding, I started with what felt like a radical approach: a pure design phase.&lt;/p&gt;
&lt;p&gt;(Now, to be clear, it's not exactly radical - lots of people have said you should write requirements first. But most vibe coders don't actually follow this practice.)&lt;/p&gt;
&lt;p&gt;I asked AI to critique my existing prototype and propose a new architecture. What followed was intense iteration on a design document right there in the airport. I did try to continue on the plane, but JetBlue's spotty Wi-Fi made that unproductive. Most of the design thinking and iteration happened during those airport hours - no code written yet, just pure design thinking.&lt;/p&gt;
&lt;p&gt;AI proposed an interface with chat memory at the high level, with separate graph memory and list memory structures underneath. It included a visualization module (originally tailored for graphs) and a node selector module for intelligent node selection. The design doc grew to at least 400-500 lines of markdown.&lt;/p&gt;
&lt;p&gt;The beauty of this approach? I could look at the prospective code in markdown blocks and play through scenarios in my head. How would someone use this API? How would the internals work? By asking very specific "how" questions, I could probe deeper and make sure I truly understood and agreed with every design choice.&lt;/p&gt;
&lt;p&gt;The major breakthrough came when I scrutinized the design and asked: why do we have two chat memory implementations, one for linear memory and one for graph memory?&lt;/p&gt;
&lt;p&gt;The natural follow-up hit me: lists are just linear graphs, so why do I need two separate structures? I can just have one that defaults to a linear graph, and then use an LLM for intelligent node selection in the threaded case.&lt;/p&gt;
&lt;p&gt;So I generalized everything to use NetworkX graphs underneath, with intelligent node selection for threaded memory. This single insight simplified the entire architecture.&lt;/p&gt;
&lt;p&gt;This is exactly what I mean about earning your automation - I could inject my own opinions into the design because I understood the problem space. We were iterating on a design doc, not just generating code.&lt;/p&gt;
&lt;h2 id="the-real-power-ai-as-a-critical-thinking-amplifier"&gt;The real power: AI as a critical thinking amplifier&lt;/h2&gt;&lt;p&gt;Here's where things got really powerful. After creating that 400-500 line design document, I had way too much detail to synthesize mentally. Time to leverage one of AI's core strengths: knowledge retrieval and pattern matching.&lt;/p&gt;
&lt;p&gt;I commanded the AI: "Go look for any inconsistencies you can see within the doc. Pick out all inconsistencies and surface them for me."&lt;/p&gt;
&lt;p&gt;This is where the magic happened. AI surfaced seven or eight inconsistencies, some I agreed with, others I dismissed as inconsequential. But because I'd just reviewed everything, it was all fresh in my mind - I could make informed decisions about each point.&lt;/p&gt;
&lt;p&gt;Then I asked it to check one more time: "Double check for me. Do you see any more inconsistencies?"&lt;/p&gt;
&lt;p&gt;Now, I wasn't fully offloading this work to AI. I was still doing synthesis in my head, trying to catch things myself. In fact, I caught an inconsistency the AI missed - sometimes I was using &lt;code&gt;bot.memory&lt;/code&gt; and sometimes &lt;code&gt;bot.chat_memory&lt;/code&gt; between the documentation and API, while continually refining and reviewing the documentation.&lt;/p&gt;
&lt;p&gt;The key insight here is about inversion - one of the core skills of critical thinking. The usual lazy pattern is to just assume things are correct (what I call "vibe coding"). But with AI assistance, we should invert and ask, "What if it's not correct?"&lt;/p&gt;
&lt;p&gt;If it's not correct, the logical follow-up becomes: can I get AI to tell me where it's wrong? This combines inversion with one of AI's key strengths - knowledge retrieval. Yes, AI struggles with needle-in-haystack problems, but for big needles in smaller haystacks? It's incredibly powerful.&lt;/p&gt;
&lt;p&gt;The "needle" here is: where am I self-contradictory? Where am I discordant? Where is my design not self-coherent? All those assumptions I might have about text-based work can be checked using AI as a tool for critical thinking.&lt;/p&gt;
&lt;p&gt;If in doubt, always invert - and now we have a lightning-fast tool for helping us do exactly that.&lt;/p&gt;
&lt;p&gt;This principle became the foundation for everything that followed.&lt;/p&gt;
&lt;h2 id="putting-the-method-into-practice-tests-first-then-code"&gt;Putting the method into practice: tests first, then code&lt;/h2&gt;&lt;p&gt;With the design doc solid, it was time for the next phase. I told the AI: "Go write the tests. Write all the tests. Follow the directory structure. Make sure the test structure matches what you're proposing."&lt;/p&gt;
&lt;p&gt;I reviewed every single test - lots of code review. But here's what's cool about AI-generated tests: they don't tend to be complicated. They're usually on the simpler side. I don't see parameterized tests using property-based testing like Hypothesis. Instead, I see example-based tests.&lt;/p&gt;
&lt;p&gt;As a first pass, example-based tests are perfect - they're concrete, easy to grasp, and I can have confidence that if the test is testing what I think it should test, then it'll pass when the implementation is written.&lt;/p&gt;
&lt;p&gt;The test review process was lightning-fast because I was so grounded in what the code was supposed to do. The design doc grounded the tests, the tests would ground the implementation. Each layer validated the next. This is the "earn your automation" principle in action - I could review tests quickly because I understood what the code should do.&lt;/p&gt;
&lt;h2 id="when-things-break-and-why-that-s-exactly-what-you-want"&gt;When things break (and why that's exactly what you want)&lt;/h2&gt;&lt;p&gt;When I finally had AI generate the implementation code and ran the tests, a lot failed - and I was totally okay with that. The first pass had maybe 20+ failing tests, but I figured out an efficient way to iterate through them in batches.&lt;/p&gt;
&lt;p&gt;I literally copied and pasted &lt;code&gt;pytest&lt;/code&gt; output and got AI to categorize the failures by common patterns. AI is blazing fast at pattern recognition - what would take me ages to figure out was near instantaneous for AI.&lt;/p&gt;
&lt;p&gt;Categorizing the failures was key. If I could group them, I could knock out three, four, sometimes even seven failing tests with targeted code changes. Even better, sometimes the failures revealed misunderstandings - either mine about the code or the AI's about the design. This forced clarifying decisions that resolved the discordance between what the test expected versus what the code actually did.&lt;/p&gt;
&lt;p&gt;With this approach, I quickly narrowed those 20+ failing tests down to maybe three or four individual syntax errors. Finally, everything worked - all tests passed, discordances resolved, ready to ship.&lt;/p&gt;
&lt;p&gt;Remember that inversion principle I mentioned earlier? This is how it played out in practice. Instead of assuming the generated code was correct, I actively looked for where it was wrong and used AI to help categorize and fix the problems systematically.&lt;/p&gt;
&lt;h2 id="the-payoff-two-days-from-design-to-deployment"&gt;The payoff: two days from design to deployment&lt;/h2&gt;&lt;p&gt;The timeline tells the whole story. I flew on Sunday morning, starting this work while at the airport, and by Monday evening had the pull request done and up to my expectations. The entire implementation phase - from final design doc to merged pull request - took just two days.&lt;/p&gt;
&lt;p&gt;But this compressed timeline was only possible because of all the preparation: four months marinating on the idea, one week during the conference to write the prototype and let it simmer while in Seattle and Tacoma, then intense design iteration with AI assistance.&lt;/p&gt;
&lt;p&gt;This teaches us something crucial about AI-assisted development: AI doesn't replace thinking and preparation - it amplifies it. I had a crystal-clear goal of what needed shipping after all that prep work. Once I was done with the prototype phase and figuring out the actual problem, bam - two days to ship.&lt;/p&gt;
&lt;p&gt;That's incredible. But notice what made this possible: not AI magic, but AI amplifying months of preparation and struggle.&lt;/p&gt;
&lt;h2 id="what-i-actually-built-and-why-it-matters"&gt;What I actually built (and why it matters)&lt;/h2&gt;&lt;p&gt;As someone who has worked with graphs before, in my eyes, the result is beautiful. Conversations are now represented as graphs, and since I work exclusively in Marimo notebooks, I can run and view Mermaid diagrams right inline. With a Mermaid diagram in a Marimo notebook, it's incredibly powerful - I can actually jump around conversation threads using the graph as visual memory to continue probing the AI system in sophisticated ways.&lt;/p&gt;
&lt;p&gt;&lt;img src="graph-memory.webp" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What I love about this implementation is that it's not just a technical achievement - it's become a practical thinking tool. The visual graph helps me navigate complex AI conversations and switch between threads mentally more easily.&lt;/p&gt;
&lt;p&gt;And I could only build this effectively because I'd earned the right to automate through that initial prototype struggle.&lt;/p&gt;
&lt;h2 id="how-this-approach-scales-the-power-of-ai-assisted-pair-coding"&gt;How this approach scales: the power of AI-assisted pair coding&lt;/h2&gt;&lt;p&gt;I have a hypothesis that this approach works even better with two people and an AI assistant - but not more than two, because you can't have too many cooks. At Moderna's Data Science and AI teams, we instituted pair coding early on so we could help each other and share knowledge. Yes, we get less done in the same time, but in the long run, we move much faster. This shared knowledge means I can quickly jump into someone else's codebase.&lt;/p&gt;
&lt;p&gt;Pair coding as a practice needs maintenance though - I noticed recently I was getting isolated into solo coding. But during my Seattle trip, I experienced pair coding with AI assistance alongside my colleague Dan Luu from the ML Platform Team. We were learning prompting tips from each other, and it was incredible - we had a chance to share practices for how to use AI to amplify ourselves.&lt;/p&gt;
&lt;p&gt;What used to be "here's how you write the function" became sharing how we're actually thinking. We've elevated the level at which we share knowledge. As Dan prompts the AI or I prompt the AI, we're learning how each other thinks in a way that's smooth, fluent, and not bogged down by syntax or implementation details. It operates at a higher plane than mere code.&lt;/p&gt;
&lt;p&gt;This is incredibly powerful because we're sharing practices for how to use AI to amplify ourselves, learning prompting techniques from each other in real time.&lt;/p&gt;
&lt;p&gt;What used to require teaching syntax and implementation details now becomes sharing thinking patterns and problem-solving approaches. We've elevated the conversation.&lt;/p&gt;
&lt;h2 id="the-pattern-that-changes-everything"&gt;The pattern that changes everything&lt;/h2&gt;&lt;p&gt;What made this approach work wasn't AI magic - it was a specific sequence that amplified months of preparation into two days of execution.&lt;/p&gt;
&lt;p&gt;First, I had to struggle. Building that fragile prototype by hand taught me what the real problems were. Without that lived experience, I couldn't have meaningfully critiqued AI's suggestions or made good design decisions. You can't skip this step.&lt;/p&gt;
&lt;p&gt;Then I could partner strategically with AI. Instead of using it as a code generator, I used it as a critical thinking amplifier. The inversion principle became key - actively asking "what's wrong here?" and leveraging AI's pattern recognition to find inconsistencies and categorize problems.&lt;/p&gt;
&lt;p&gt;Finally, I followed a systematic progression: design document first, then comprehensive tests, then implementation. When tests inevitably failed, I used AI to categorize failures and fix them in batches rather than one by one.&lt;/p&gt;
&lt;p&gt;The two days it took me to ship graph memory weren't about AI being magical. They were about using AI properly after doing the hard work of earning that automation. The months of struggle weren't wasted time - they were the essential foundation that made AI partnership effective.&lt;/p&gt;
&lt;p&gt;This is how you go from vibe coding to strategic automation. Not by delegating thinking to AI, but by using AI to amplify the thinking you've already earned the right to do.&lt;/p&gt;
</content></entry><entry><title>How to use xarray for unified laboratory data storage</title><link href="https://ericmjl.github.io/blog/2025/7/15/how-to-use-xarray-for-unified-laboratory-data-storage/" rel="alternate"/><updated>2025-07-15T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:ab8f5511-7289-3d99-b9e8-026ea9a58088</id><content type="html">&lt;p&gt;What if your laboratory and machine learning related data could be managed within a single data structure? From raw experimental measurements to computed features to model outputs, everything coordinate-aligned and ready for analysis.&lt;/p&gt;
&lt;p&gt;I've been thinking about this problem across different experimental contexts. We generate measurement data, then computed features, then model outputs, then train/test splits. Each piece typically lives in its own file, its own format, with its own indexing scheme. The cognitive overhead of keeping track of which sample corresponds to which row in which CSV is exhausting.&lt;/p&gt;
&lt;p&gt;Let me illustrate this with a microRNA expression study as a concrete example.&lt;/p&gt;
&lt;p&gt;Here's an approach that could solve this: &lt;strong&gt;store everything in a unified xarray Dataset where sample identifiers are the shared coordinate system&lt;/strong&gt;. Your experimental measurements, computed features, statistical estimates, and data splits all aligned by the same IDs. No more integer indices. No more file juggling. Just clean, coordinated data that scales to the cloud.&lt;/p&gt;
&lt;h2 id="what-s-wrong-with-traditional-laboratory-data-management"&gt;What's wrong with traditional laboratory data management?&lt;/h2&gt;&lt;p&gt;Picture this: you're three months into a microRNA expression study. You've got the following files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;expression measurements in &lt;code&gt;expression_data.csv&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;ML features in &lt;code&gt;sequence_features.parquet&lt;/code&gt;,&lt;/li&gt;
&lt;li&gt;model outputs in &lt;code&gt;model_results.h5&lt;/code&gt;, and&lt;/li&gt;
&lt;li&gt;train/test splits scattered across &lt;code&gt;train_indices.npy&lt;/code&gt; and &lt;code&gt;test_indices.npy&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each file has its own indexing scheme - some use row numbers, others use identifiers, and you're constantly writing index-matching code just to keep everything aligned.&lt;/p&gt;
&lt;p&gt;The cognitive overhead is brutal. Which microRNA corresponds to row 47 in the features file? Did you remember to filter out the same samples from both your training data and your metadata? When you subset your data for analysis, do all your indices still match?&lt;/p&gt;
&lt;p&gt;I've lost count of how many times I've seen analysis pipelines break because someone forgot to apply the same filtering to all their data files. It's not just inefficient - it's error-prone and exhausting.&lt;/p&gt;
&lt;h2 id="how-does-xarray-solve-this"&gt;How does xarray solve this?&lt;/h2&gt;&lt;p&gt;Xarray changes the game by making &lt;strong&gt;coordinates the foundation of your data structure&lt;/strong&gt;. Instead of managing separate files with separate indexing schemes, you create one unified dataset where &lt;em&gt;every piece of data knows exactly which microRNA it belongs to&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;The beauty lies in the coordinate system. Each data point is labeled with meaningful coordinates: not just row numbers, but actual experimental factors like microRNA ID, treatment condition, time point, and replicate. When you slice your data, everything stays aligned automatically.&lt;/p&gt;
&lt;p&gt;This is transformative! When everything shares the same coordinate system, you can slice across any dimension and everything stays connected. Want features for specific microRNAs? The model results for those same microRNAs come along automatically.&lt;/p&gt;
&lt;h2 id="what-does-unified-data-storage-look-like"&gt;What does unified data storage look like?&lt;/h2&gt;&lt;p&gt;Let me walk you through how this works in practice. We start with a coordinate system that captures the experimental design:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Coordinates:
* mirna           (150 microRNAs: hsa-miR-1, hsa-miR-2, ...)
* treatment       (3 conditions: control, hypoxia, inflammation)
* time_point      (5 timepoints: 2h, 6h, 12h, 24h, 48h)
* replicate       (3 replicates: rep_1, rep_2, rep_3)
* cell_line       (10 cell lines: cell_line_01, cell_line_02, ...)
* experiment_date (4 dates: experiment dates)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then we progressively add data that aligns with these coordinates:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Stage 1: Expression measurements&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dataset&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;expression_level&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;expression_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Stage 2: Bayesian estimation results&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mirna_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mirna_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mirna_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mirna_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;treatment_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;treatment_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;treatment_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;treatment_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;time_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;time_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;time_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;time_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;replicate_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;replicate_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;replicate_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;replicate_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;cell_line_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cell_line_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;cell_line_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cell_line_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;# Stage 3: ML features&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;ml_features&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;feature&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;feature_matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign_coords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nt_A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;length&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;gc_content&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Stage 4: Train/test splits&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;train_mask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;split_type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;train_masks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;test_mask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;split_type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;test_masks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The magic happens when you realize that &lt;strong&gt;every piece of data is automatically aligned by the shared coordinate system&lt;/strong&gt;. Need to analyze expression patterns for microRNAs in your training set? It's just coordinate selection:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Get training mask for random 80/20 split&lt;/span&gt;
&lt;span class="n"&gt;train_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train_mask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;split_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;random_80_20&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Get ML features for training microRNAs&lt;/span&gt;
&lt;span class="n"&gt;train_features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ml_features&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Get expression data for the same microRNAs&lt;/span&gt;
&lt;span class="n"&gt;train_expression&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expression_level&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_mask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Everything stays connected automatically. No manual bookkeeping required.&lt;/p&gt;
&lt;h2 id="how-do-we-build-this-step-by-step"&gt;How do we build this step by step?&lt;/h2&gt;&lt;p&gt;The approach is straightforward - &lt;strong&gt;progressive data accumulation&lt;/strong&gt;. You don't need to have everything figured out upfront. Start with your core experimental data, then add layers as your analysis develops.&lt;/p&gt;
&lt;h3 id="stage-1-laboratory-measurements"&gt;Stage 1: Laboratory measurements&lt;/h3&gt;&lt;p&gt;Your foundation is the experimental data with meaningful coordinates:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Expression data automatically aligned by coordinates&lt;/span&gt;
&lt;span class="n"&gt;expression_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;measurements&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mirna_ids&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;control&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;hypoxia&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;inflammation&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;rep_1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rep_2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rep_3&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;2h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;6h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;12h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;24h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;48h&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cell_lines&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You should note here how the coordinates basically mirror the experimental design.&lt;/p&gt;
&lt;h3 id="stage-2-bayesian-estimation"&gt;Stage 2: Bayesian estimation&lt;/h3&gt;&lt;p&gt;Add effect estimates that align with your experimental coordinates:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Bayesian effects model results&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mirna_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mirna_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;mirna_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;mirna_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;treatment_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;treatment_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;treatment_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;treatment&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;treatment_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;time_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;time_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;time_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;time_point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;time_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;replicate_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;replicate_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;replicate_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;replicate&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;replicate_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;cell_line_effects&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cell_line_coefficients&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;cell_line_effects_std&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;cell_line&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cell_line_coefficient_errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The beauty is that your Bayesian effects model estimates align perfectly with your experimental design coordinates. Each experimental factor gets its own effect estimate with uncertainty, organized by the same coordinate system as your raw data.&lt;/p&gt;
&lt;h3 id="stage-3-ml-features"&gt;Stage 3: ML features&lt;/h3&gt;&lt;p&gt;Features slot right into the same coordinate system:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# ML features aligned by microRNA ID&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;ml_features&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;feature&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;feature_matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign_coords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nt_A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_G&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;nt_C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;length&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;gc_content&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="stage-4-train/test-splits"&gt;Stage 4: Train/test splits&lt;/h3&gt;&lt;p&gt;Even data splits become part of the unified structure:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Boolean masks aligned by microRNA coordinate&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;train_mask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;split_type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;train_masks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;test_mask&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mirna&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;split_type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;test_masks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Progressive build = reduced cognitive load&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The beauty of this approach is that you can build it incrementally. Start with your core experimental data, then add statistical results, then ML features, then splits. Each stage builds on the previous coordinate system, so everything stays aligned automatically.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="what-are-the-practical-benefits"&gt;What are the practical benefits?&lt;/h2&gt;&lt;h3 id="no-more-index-juggling"&gt;No more index juggling&lt;/h3&gt;&lt;p&gt;Remember the nightmare of keeping track of which microRNA corresponds to which row in which file? That's gone. Every piece of data knows its own coordinates.&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Before: manual index matching across files&lt;/span&gt;
&lt;span class="n"&gt;expression_subset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;expression_df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;train_indices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;features_subset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;features_df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mirna_ids&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;train_indices&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;model_results_subset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model_df&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;iloc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;train_indices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# After: coordinate-based selection&lt;/span&gt;
&lt;span class="n"&gt;train_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;train_mask&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;split_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;random_80_20&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="bulletproof-data-consistency"&gt;Bulletproof data consistency&lt;/h3&gt;&lt;p&gt;When you slice your data, everything stays aligned automatically. No more worrying about applying the same filtering to all your files.&lt;/p&gt;
&lt;h3 id="cloud-native-scaling"&gt;Cloud-native scaling&lt;/h3&gt;&lt;p&gt;Store everything in Zarr format and your unified dataset becomes cloud-native. Load it from S3, query specific slices, and everything scales seamlessly. (Note: Zarr has some limitations with certain data types like U8, but xarray supports multiple storage formats to work around these issues.)&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="c1"&gt;# Save entire workflow to cloud&lt;/span&gt;
&lt;span class="n"&gt;unified_dataset&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_zarr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s3://biodata/mirna_screen_2024.zarr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Load and analyze anywhere&lt;/span&gt;
&lt;span class="n"&gt;experiment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;open_zarr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s3://biodata/mirna_screen_2024.zarr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="reproducible-analysis-pipelines"&gt;Reproducible analysis pipelines&lt;/h3&gt;&lt;p&gt;Your analysis becomes more reproducible because the data structure itself enforces consistency. Share the dataset and the analysis code just works.&lt;/p&gt;
&lt;h2 id="what-tools-make-this-possible"&gt;What tools make this possible?&lt;/h2&gt;&lt;p&gt;The tooling ecosystem has evolved dramatically in recent years. A few years ago, I would have told you to use parquet files with very unnatural tabular setups to get everything into tidy format. But &lt;strong&gt;xarray is changing the game&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Xarray&lt;/strong&gt; provides the coordinate system and multidimensional data structures that make this unified approach possible. It's like pandas for higher-dimensional data, but with meaningful coordinates instead of just integer indices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Zarr&lt;/strong&gt; gives you cloud-native storage that preserves all your coordinate information and metadata. It supports chunking, compression, and parallel access - perfect for scaling your unified datasets.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The tools we've got are just getting better and better. I wouldn't have imagined that we'd be able to use xarray for this kind of unified laboratory data storage just a few years ago. The ecosystem is maturing rapidly, and these approaches are becoming more accessible every year.&lt;/p&gt;
&lt;h2 id="what-s-next"&gt;What's next?&lt;/h2&gt;&lt;p&gt;If you're working with multidimensional experimental data, I'd strongly encourage you to try this unified approach. Start small - take your next experiment and see if you can structure it as a single xarray Dataset instead of multiple files.&lt;/p&gt;
&lt;p&gt;The cognitive overhead reduction is immediate. No more wondering if your indices are aligned. No more writing index-matching code. Just clean, coordinated data that scales to the cloud.&lt;/p&gt;
&lt;p&gt;Time will distill the best practices in your context, but I've found this unified approach eliminates so much friction from the experimental data lifecycle. Give it a try and see how it feels in your workflow.&lt;/p&gt;
&lt;p&gt;I cooked up this synthetic example while attending Ian Hunt-Isaak's talk &lt;a href="https://cfp.scipy.org/scipy2025/talk/AARA39/"&gt;"Xarray across biology. Where are we and where are we going?"&lt;/a&gt; at SciPy 2025. His presentation on using xarray for biological data really crystallized how powerful this coordinate-based approach could be for the typical experimental workflow.&lt;/p&gt;
</content></entry><entry><title>Reflections on the SciPy 2025 Conference</title><link href="https://ericmjl.github.io/blog/2025/7/14/reflections-on-the-scipy-2025-conference/" rel="alternate"/><updated>2025-07-14T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:65ab7a38-7622-37a4-bce5-8af2c9cb1881</id><content type="html">&lt;p&gt;This year marks my 10th year of being involved with the Scientific Python Conference, and it has been an absolute blast! What started as curiosity about the intersection of science and software has grown into a decade of learning, teaching, and contributing to this incredible community.&lt;/p&gt;
&lt;h2 id="conference-activities-summary"&gt;Conference Activities Summary&lt;/h2&gt;&lt;p&gt;This year's SciPy was particularly active for me. I taught two tutorials: "Building with LLMs Made Simple" (a new one) and "Network Analysis Made Simple" (my longtime favorite). After the tutorials, I attended several inspiring talks, including an especially motivating presentation on XArray in biology that prompted me to create a Marimo notebook demonstrating XArray's applications in biological data analysis.&lt;/p&gt;
&lt;p&gt;One of my favorite conference activities this year was recording conversations with fellow attendees. In lieu of my Insta360 camera, I brought my DJI mic everywhere and captured numerous insightful discussions, creating an informal podcast collection of SciPy conversations. Finally, during the sprints, I felt more tapped out than usual but still managed to contribute to Llamabot development with others and work on the XArray biology materials I had envisioned.&lt;/p&gt;
&lt;h2 id="tutorials"&gt;Tutorials&lt;/h2&gt;&lt;h3 id="building-with-llms-made-simple"&gt;Building with LLMs Made Simple&lt;/h3&gt;&lt;p&gt;This was my first time teaching this tutorial, and I was thrilled to use Marimo notebooks throughout the entire session. The tutorial covered three main areas: simple LLM interactions, structured generation, and RAG (Retrieval-Augmented Generation). You can find the tutorial materials at: &lt;a href="https://github.com/ericmjl/building-with-llms-made-simple"&gt;https://github.com/ericmjl/building-with-llms-made-simple&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The structured generation section was particularly powerful. I emphasized that structured generation is fundamentally about automating form-filling using natural language. Having free text input and getting a filled-out Pydantic model output is incredibly valuable for productivity. One participant mentioned the concept of automating "the dangerous, the dull, and the dirty" - which perfectly captures how LLMs can handle routine tasks.&lt;/p&gt;
&lt;p&gt;For RAG, I clarified that RAG doesn't necessarily equal vector databases - it's about information retrieval through various means including keyword search. I demonstrated custom chunking strategies for standard operating procedures, showing how simple solutions (like appending source references) often work better than complex hierarchical structures.&lt;/p&gt;
&lt;p&gt;The tutorial concluded with brief demos on evaluation and agents. I shared my experience testing different models (Gemma, Llama 3, Llama 4) for docstring generation, emphasizing the importance of experimentation and model selection. For agents, I stressed starting with simpler structured generation approaches before building complex autonomous systems.&lt;/p&gt;
&lt;p&gt;Thanks to Modal's generous credit allocation from their DevRel Charles, I was able to deploy an Ollama endpoint in the cloud, making the tutorial accessible to all participants.&lt;/p&gt;
&lt;h3 id="network-analysis-made-simple"&gt;Network Analysis Made Simple&lt;/h3&gt;&lt;p&gt;This marked either my ninth or tenth time teaching this tutorial at SciPy - my longtime favorite. This year I made the significant transition from Jupyter to Marimo notebooks, which was an experiment that generally worked well despite some setup challenges. You can find the tutorial materials at: &lt;a href="https://github.com/ericmjl/Network-Analysis-Made-Simple"&gt;https://github.com/ericmjl/Network-Analysis-Made-Simple&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The tutorial faced some technical hurdles for installation with the Network Analysis Made Simple package being published on my own PyPI server, plus some participants weren't familiar with Marimo. Fortunately, Erik Welch from NVIDIA was present to help assist participants. By the end of the conference talk days, I was able to resolve the issue by changing the notebooks to draw from the Network Analysis Made Simple source directly instead of my own PyPI server, which solved most of the installation problems.&lt;/p&gt;
&lt;p&gt;What I loved most was the audience engagement. We didn't cover as much content as usual because participants asked so many thoughtful questions, especially during the visualization section. This interaction made the session incredibly valuable, as people were clearly learning and developing new ideas for their own work.&lt;/p&gt;
&lt;p&gt;The Marimo experiment succeeded in shifting the learning environment with minimal overhead. For future iterations, I'm considering eliminating the separate NAMS package and making the entire notebook self-contained with answers included at the bottom.&lt;/p&gt;
&lt;h3 id="overarching-thoughts-on-the-tutorials"&gt;Overarching thoughts on the tutorials&lt;/h3&gt;&lt;p&gt;Both tutorials were conducted entirely within Marimo notebooks, which convinced quite a few participants to switch over to Marimo. They saw the power of fully reactive notebooks and the ability to seamlessly share analysis from one person to another - something that's much more cumbersome with traditional Jupyter notebooks.&lt;/p&gt;
&lt;p&gt;Both tutorials will also be available on YouTube! There was a technical glitch with the Building with LLMs Made Simple tutorial recording, so I'm planning to re-record the full tutorial this coming Saturday - including content we didn't get to cover during the live session. This should actually result in a better, more complete recording for the YouTube release, which I'll also release to my own channel.&lt;/p&gt;
&lt;h2 id="talks-and-presentations"&gt;Talks and Presentations&lt;/h2&gt;&lt;p&gt;I attended several inspiring talks throughout the conference. Here are short summaries of the key presentations that caught my attention:&lt;/p&gt;
&lt;h3 id="xarray-in-biology-ian-hunt-isaak"&gt;XArray in Biology (Ian Hunt-Isaak)&lt;/h3&gt;&lt;p&gt;This talk was particularly inspiring and prompted me to create a Marimo notebook demonstrating XArray applications in biology. Ian, a biologist and microscopist from Earthmover (funded by the Chan Zuckerberg Initiative), presented a compelling case for XArray adoption in biological research.&lt;/p&gt;
&lt;p&gt;XArray excels at handling multi-dimensional biological data like time-series microscopy images, multi-channel fluorescent data, and complex experimental metadata. Its semantic indexing capabilities (e.g., &lt;code&gt;data.sel(time='30.5min', field_of_view=1, channel='GFP').max('z')&lt;/code&gt;) make biological data analysis much more intuitive.&lt;/p&gt;
&lt;p&gt;Despite its benefits, XArray has seen limited adoption in biology due to awareness barriers and lack of biology-specific examples. Recent improvements like DataTree for hierarchical data structures and flexible indices for complex coordinate systems address many biological data needs. The roadmap includes developing biology-specific documentation and building a user community within the next year.&lt;/p&gt;
&lt;h3 id="scipy-statistical-distributions-infrastructure-albert-steppi"&gt;SciPy Statistical Distributions Infrastructure (Albert Steppi)&lt;/h3&gt;&lt;p&gt;Albert, one of SciPy's maintainers, presented the complete rewrite of SciPy's statistical distributions framework, primarily designed by Matt Haberland. The new infrastructure addresses significant limitations of the old system, including memory leaks, inflexible documentation, and parameter processing overhead.&lt;/p&gt;
&lt;p&gt;Key improvements include a single consistent API where distributions are classes users instantiate, better performance, arithmetic operations on distributions (shifting, scaling, transformations), and simplified custom distribution creation. Future development will focus on distribution-specific fitting methods and support for alternative array backends like PyTorch and JAX.&lt;/p&gt;
&lt;h3 id="high-level-api-dispatching-for-community-scaling-erik-welch"&gt;High-Level API Dispatching for Community Scaling (Erik Welch)&lt;/h3&gt;&lt;p&gt;This presentation explored how dispatching enables scaling of open source communities while managing contributor burden. The speaker shared implementation experiences with NetworkX (3-year evolution from pure Python to supporting faster implementations) and Scikit-Image (1-year implementation dispatching to NVIDIA cuCIM).&lt;/p&gt;
&lt;p&gt;The talk emphasized community engagement importance, careful bandwidth management, and maintaining balance between users, library maintainers, and backend developers. While dispatching is "deceptively simple," it requires careful consideration of nuanced implementation choices.&lt;/p&gt;
&lt;h3 id="marimo-the-future-of-notebooks-akshay-agrawal"&gt;Marimo: The Future of Notebooks (Akshay Agrawal)&lt;/h3&gt;&lt;p&gt;I was thrilled to see Marimo's founder Akshay give a talk about the future of notebooks. His live demo showcasing all of Marimo's capabilities was as gutsy as my own Data-Driven Pharma talk (which was also done entirely in a Marimo notebook).&lt;/p&gt;
&lt;p&gt;The fundamental change Marimo has brought to my workflow has been amazing. Not having to specify a separate manifest file for dependencies like with Jupyter notebooks was one of the big selling points for me. We had dinner together with a large group and got to discuss Marimo's future development - it was awesome to meet him in person and share thoughts on where the platform is heading.&lt;/p&gt;
&lt;h2 id="recording-conversations-and-networking"&gt;Recording Conversations and Networking&lt;/h2&gt;&lt;p&gt;One of my favorite activities this year was bringing my DJI mic everywhere and recording conversations with fellow attendees. Over the years, I've realized how informative and valuable these SciPy conversations are, so I decided to capture them as informal podcast content.&lt;/p&gt;
&lt;p&gt;The first recording happened over breakfast with Hugo Bowne-Anderson. We were discussing everything while eating salmon frittata - we now call it "the frittata chat." Hugo loved the idea so much that he sent it to his editor, and it will appear on his podcast "Vanishing Gradients" soon.&lt;/p&gt;
&lt;p&gt;I continued this approach with Daniel Chen (my conference doppelganger - we get mistaken for each other at every conference) and Ryan Cooper. I also had an incredible hour-and-twenty-minute conversation with Zweli, covering topics from Bayes and graphs to apartheid and parenting. While I missed some talks due to these extended conversations, that's often the real purpose of conferences - engaging in dialogue we don't usually get to have.&lt;/p&gt;
&lt;p&gt;Whether I'll release these as formal podcast episodes depends partly on my energy levels and whether the participants agree, but the conversations themselves provided immense value and captured knowledge I didn't want to lose.&lt;/p&gt;
&lt;h2 id="nerd-sniping-and-code-reviews"&gt;Nerd Sniping and Code Reviews&lt;/h2&gt;&lt;p&gt;I got thoroughly nerd-sniped by Joe Cheng, CTO of Posit, who found Llamabot and conducted an impromptu code review. We first met at the NVIDIA event while I was recording a conversation with Daniel Chen about AI education and assessment.&lt;/p&gt;
&lt;p&gt;Joe had recently decided that generative AI was a productive area for Posit and found Llamabot during his research. Standing outside the Glass Museum for half an hour, he grilled me with questions about design choices I'd never had the chance to discuss with anyone before. The nerd sniping continued over ramen takeout in the hotel lobby from Thekoi (awesome restaurant by the way!), where he asked about corners of the codebase with the thoroughness of a technical interview that I've subjected multiple people to. Talk about karma!&lt;/p&gt;
&lt;p&gt;Joe also ended up nerd sniping himself during our discussions and built something with the OpenAI real-time API that he showed me on Thursday evening. It was incredibly fun - we were on his computer together, nerding out about tweaking the real-time API settings to fit a user experience that would work with my brain, where I take a bit more time to respond and don't necessarily like the rapid-fire conversation turns.&lt;/p&gt;
&lt;p&gt;This nerd sniping cascade had a knock-on effect: it led me to implement graph-based memory for Llamabot, which then revealed that the chat memory API really wasn't optimal and needed another rewrite. There's now a 0.13 release of Llamabot planned in my head that will need to happen soon - all thanks to Joe's infectious curiosity and builder mentality!&lt;/p&gt;
&lt;h2 id="sprints"&gt;Sprints&lt;/h2&gt;&lt;p&gt;The sprints provided a chance to contribute to open source projects, though I felt more tapped out than usual this year. Despite the fatigue, I managed to make meaningful contributions to three key areas.&lt;/p&gt;
&lt;h3 id="llamabot-development"&gt;Llamabot Development&lt;/h3&gt;&lt;p&gt;Joe Cheng's nerd sniping during the conference led me to spend time during the sprints implementing graph-based memory for Llamabot. The challenge was representing conversation turns as pairs of human and AI messages while inferring the most probable message that a human is responding to when creating new branches in the conversation.&lt;/p&gt;
&lt;p&gt;I successfully implemented this graph memory system, which required determining how to connect new human messages to existing assistant messages in the conversation graph. This feature allows for more sophisticated conversation tracking and branching compared to traditional linear chat histories. You can see the implementation in this pull request: &lt;a href="https://github.com/ericmjl/llamabot/pull/226"&gt;https://github.com/ericmjl/llamabot/pull/226&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="xarray-biology-contributions"&gt;XArray Biology Contributions&lt;/h3&gt;&lt;p&gt;Inspired by Ian's talk on XArray in biology, I worked on creating Marimo notebook examples demonstrating how XArray can be effectively used for biological data analysis. This contribution aims to bridge the gap between XArray's powerful capabilities and the biology community's needs for multi-dimensional data handling.&lt;/p&gt;
&lt;p&gt;The goal was to provide concrete examples that biologists could use as starting points for their own projects, helping to increase XArray adoption in biological research by making its benefits more tangible and accessible. You can find the completed notebook at: &lt;a href="https://gist.github.com/ericmjl/e5b267782f9cbd27f712153deab426e1"&gt;https://gist.github.com/ericmjl/e5b267782f9cbd27f712153deab426e1&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="teen-track-talk"&gt;Teen Track Talk&lt;/h3&gt;&lt;p&gt;Inessa Pawson asked if I would be willing to give a talk to the teens attending the conference. I shared stories about building your own tools and recounted experiences from my career journey. I told them how I got in through the back door and walked out through the front door of grad school, emphasizing how much you can learn along the way.&lt;/p&gt;
&lt;p&gt;Using the same approach from my Data-Driven Pharma talk, I showed them how I can build my own tools without relying on PowerPoint - by showing them live that I built my own slide deck generator. I shared how I picked up programming and made 70+ pull requests with the Matplotlib team, which was an incredible learning experience, and how the learning experience helped me later professionally at Novartis and Moderna, where being able to build tools for myself helped me be the change I wanted to see in the world. The goal was to inspire them to see that they too can build their own tools and, perhaps, be the change they wanted to see.&lt;/p&gt;
&lt;h2 id="conference-tidbits"&gt;Conference Tidbits&lt;/h2&gt;&lt;p&gt;A few smaller moments that captured the spirit of SciPy and the power of modern notebook sharing: I helped Hugo with a quick analysis during the conference and was able to simply airdrop him a Marimo notebook with the complete analysis. The fact that I could share a fully self-contained, executable analysis so seamlessly really demonstrated how far we've come in making scientific computing more collaborative and accessible.&lt;/p&gt;
&lt;p&gt;Another remarkable tidbit: I went to Chili Thai for the sixth and seventh time in two years, which is pretty remarkable considering that I've only been at the conference for a total of 14 days. Chili Thai really earns high ratings from me - the duck curry and the panang curry are amongst the best I've ever had.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;&lt;p&gt;Attending the SciPy conference for about a decade now has been an immense resource for my career growth. Beyond being a participant, I've also been involved as an organizer, serving on the financial aid committee for almost a decade. It's my little way of giving back to a community that has given me so much, and I'm always looking for ways to contribute even more.&lt;/p&gt;
&lt;p&gt;What makes SciPy special is its incredible community of people who are curious, nerdy, and remarkably ego-free. There's a genuine spirit of learning and teaching - many are educators at heart, eager to share knowledge and help others grow. This creates an environment where meaningful connections and learning happen naturally.&lt;/p&gt;
&lt;p&gt;I'd really recommend more people attend SciPy if their company finances allow for it. The value you get from the tutorials, talks, networking, and collaborative spirit is immense. However, I do know from helping organize the conference that this year we ran at a deficit, which isn't financially sustainable. I hope we can find more sponsors for next year to keep this amazing event accessible.&lt;/p&gt;
&lt;p&gt;If possible, I'd love to help sponsor the conference, especially the Financial Aid program. Being able to bring new people to the conference - particularly community contributors who have demonstrated need - would be amazing. I was a beneficiary of financial aid myself early in my career, and it made all the difference in my ability to participate and grow within this community.&lt;/p&gt;
&lt;p&gt;The SciPy conference continues to be a cornerstone of my professional development and a source of inspiration for pushing the boundaries of what's possible with scientific computing!&lt;/p&gt;
</content></entry><entry><title>Earn the privilege to use automation</title><link href="https://ericmjl.github.io/blog/2025/7/13/earn-the-privilege-to-use-automation/" rel="alternate"/><updated>2025-07-13T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:ab61e0b2-3ffb-313f-b904-63d2b3293835</id><content type="html">&lt;p&gt;AI in education was supposed to be transformative.&lt;/p&gt;
&lt;p&gt;We imagined students with AI tutors available 24/7, personalized learning at scale, and democratized access to high-quality education. The promise was intoxicating: every student could have their own Socrates, guiding them through complex concepts with infinite patience.&lt;/p&gt;
&lt;p&gt;Then reality hit.&lt;/p&gt;
&lt;h2 id="when-ai-integration-fails-spectacularly"&gt;When AI integration fails spectacularly&lt;/h2&gt;&lt;p&gt;Lorena Barba, a respected engineering professor at George Washington University, shared her experience at SciPy 2025 of deciding to fully embrace AI in her computational engineering course. She built a custom AI tool with her technical partners, complete with document upload capabilities, retrieval augmented generation, and safety moderation features. She gave her students what seemed like the perfect educational AI assistant.&lt;/p&gt;
&lt;p&gt;The results were devastating.&lt;/p&gt;
&lt;p&gt;Her course evaluations plummeted from 4.8/5 to 2.3/5. Students stopped attending class. They stopped doing homework with any rigor. Some copied entire assignment questions, including instructions like "your code here," and expected complete answers they could submit without understanding.&lt;/p&gt;
&lt;p&gt;The most damning feedback? Students told her: "I would have learned better if AI were not present."&lt;/p&gt;
&lt;p&gt;What went wrong? Lorena had given her students unbridled access to AI without ensuring they had the foundational skills to use it effectively. Students developed what she called an "illusion of competence"—they overestimated their knowledge because AI made everything feel easy. They missed the deep processing necessary for long-term memory formation.&lt;/p&gt;
&lt;p&gt;After 20 years of successful teaching, Lorena experienced what she called a "frustrating, humbling failure." She's now considering returning to oral examinations to preserve assessment authenticity.&lt;/p&gt;
&lt;h2 id="the-assessment-validity-crisis"&gt;The assessment validity crisis&lt;/h2&gt;&lt;p&gt;Lorena's experience reveals a fundamental problem: AI has broken traditional assessment methods. If students can get AI to do their work, how do we evaluate their actual understanding? How do we conduct meaningful assessments in both educational and workplace settings?&lt;/p&gt;
&lt;p&gt;This question hits close to home for me. As a team lead, I constantly assess whether candidates are ready for the job and whether my teammates are performing at expected levels. If I'm only looking at work outputs—the final code, the completed analysis, the polished presentation—that's an inadequate assessment method. AI has made it trivially easy to produce impressive-looking outputs while learning nothing.&lt;/p&gt;
&lt;p&gt;I need to understand &lt;em&gt;how&lt;/em&gt; people think through problems, not just whether they can deliver results. This challenge sparked intense conversations with educators at SciPy 2025. Daniel Chen (University of British Columbia) and Ryan Cooper (University of Connecticut) each brought unique perspectives on adapting our assessment methods to this new reality.&lt;/p&gt;
&lt;h2 id="assessing-the-process-not-just-the-product"&gt;Assessing the process, not just the product&lt;/h2&gt;&lt;p&gt;Daniel Chen had to fundamentally shift his approach. He moved up Bloom's taxonomy for assessment, focusing on questioning and synthesis rather than factual regurgitation. His key insight: &lt;strong&gt;when students ask questions, it reveals their level of understanding&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Insightful questions indicate pursuit of mastery. Surface-level "how do I get this done" questions reveal a lack of deep engagement.&lt;/p&gt;
&lt;p&gt;Daniel proposed assessing students through their AI chat transcripts. Instead of only evaluating final products, we could examine both process and outcome. This approach reveals &lt;em&gt;how&lt;/em&gt; students think through problems, potentially restoring validity to our assessments.&lt;/p&gt;
&lt;p&gt;Ryan Cooper had already started implementing this idea, collecting chat transcripts to understand student thinking patterns. He also experimented with having students generate their own exam questions—leveraging the fact that creation sits at the highest level of Bloom's taxonomy.&lt;/p&gt;
&lt;p&gt;Ryan gave students access to a curated AI system conditioned with course context, generating on-the-fly assessment questions. While innovative, he encountered challenges with rubric-based grading when AI suggested grades without clear criteria.&lt;/p&gt;
&lt;h2 id="why-this-matters-in-the-workplace"&gt;Why this matters in the workplace&lt;/h2&gt;&lt;p&gt;These educational assessment challenges directly mirror my daily reality as a team lead. AI assistance allows me to work solo and move incredibly fast—I love that turbocharged feeling. But this speed creates a dangerous blind spot that affects both my personal development and my team's growth.&lt;/p&gt;
&lt;p&gt;Here's my dilemma: if I don't slow down to demonstrate my thinking process, we lose opportunities to train junior team members. More concerning, if I can't see how my team members approach problems—only their final outputs—I can't effectively assess their capabilities or guide their development.&lt;/p&gt;
&lt;p&gt;When team members use GitHub Copilot or similar tools, I need visibility into their thought processes, not just their code. Are they asking insightful questions? Do they understand the trade-offs they're making? Can they spot when the AI suggests something problematic? Without access to their reasoning process, I'm essentially conducting performance reviews based on AI-assisted outputs rather than human capability.&lt;/p&gt;
&lt;p&gt;This visibility gap threatens knowledge transfer and continuity. We risk training a generation of practitioners who can orchestrate AI to produce impressive results but lack the foundational understanding to innovate when the tools fail or evolve.&lt;/p&gt;
&lt;h2 id="earning-the-privilege-of-automation"&gt;Earning the privilege of automation&lt;/h2&gt;&lt;p&gt;The solution to this assessment crisis—both educational and professional—isn't to ban AI tools or ignore their impact. Instead, we need a fundamental shift in how we think about automation access.&lt;/p&gt;
&lt;p&gt;Here's the central insight that crystallized from these conversations: &lt;strong&gt;people must earn the privilege to use automation&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The use of large language models for coding is automated code drafting. If you lack the skills to evaluate and verify correctness, you shouldn't use LLMs for anything important. This isn't about restricting access, but rather, it's about ensuring people develop foundational competencies first, then demonstrate those competencies before gaining access to powerful automation.&lt;/p&gt;
&lt;p&gt;The principle is straightforward: &lt;strong&gt;demonstrate you can verify AI output before using AI for critical work.&lt;/strong&gt; This means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Understanding underlying concepts well enough to spot errors&lt;/li&gt;
&lt;li&gt;Having skills to validate AI-generated solutions&lt;/li&gt;
&lt;li&gt;Developing judgment to recognize when something doesn't make sense&lt;/li&gt;
&lt;li&gt;Building fortitude to dig deeper when results seem questionable&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'm fine with "vibe coding" in unfamiliar languages for throwaway explorations—that's valuable for learning. But for work that matters, the ability to verify correctness is non-negotiable.&lt;/p&gt;
&lt;h2 id="the-path-forward"&gt;The path forward&lt;/h2&gt;&lt;p&gt;Lorena's lessons teach us that unrestricted AI access without foundational skills leads to degraded learning outcomes. We need systematic approaches to ensure people earn their automation privileges. These include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Moving assessments up Bloom's taxonomy&lt;/strong&gt; to focus on higher-order thinking skills that AI can't easily replicate&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluating process alongside product&lt;/strong&gt; through chat transcript analysis and collaborative work&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Encouraging creation and synthesis&lt;/strong&gt; rather than regurgitation—have students generate exam questions, not just answer them&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Implementing pair programming and mentoring&lt;/strong&gt; that reveals thinking patterns and preserves knowledge transfer&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Maintaining human elements&lt;/strong&gt; in learning and development to counteract AI's tendency to create isolated workers&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The future belongs to those who can effectively collaborate with AI while maintaining the critical thinking skills to guide and verify that collaboration. But they must demonstrate mastery of fundamentals before earning that privilege.&lt;/p&gt;
&lt;p&gt;We're not trying to halt progress or ban useful tools. We're ensuring that powerful automation serves human capability rather than replacing it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;With thanks to Lorena Barba (George Washington University), Daniel Chen (University of British Columbia), Ryan Cooper (University of Connecticut), and Emily Dorne (Driven Data) for sharing their experiences and insights. Their perspectives as professional educators and industry practitioners navigating AI's impact on learning, assessment, and hiring shaped these reflections.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="addendum"&gt;Addendum&lt;/h2&gt;&lt;p&gt;Lorena Barba continued this conversation at JupyterCon, delivering a talk titled &lt;a href="https://www.youtube.com/watch?v=g0X4f0tRTTo"&gt;"Teaching and Learning With Jupyter and AI: An Educator's Dilemma"&lt;/a&gt;. In this follow-up presentation, she further explores the challenges educators face when students use AI as a shortcut, creating an "illusion of competence" that undermines genuine learning. The talk addresses the impasse educators face with vague university guidance and challenges to assessment validity, building on the themes she shared at SciPy 2025.&lt;/p&gt;
</content></entry><entry><title>The job your docs need to do</title><link href="https://ericmjl.github.io/blog/2025/7/7/the-job-your-docs-need-to-do/" rel="alternate"/><updated>2025-07-07T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:3253a2a7-3c77-3b72-b320-3aa34f3c80d0</id><content type="html">&lt;h1 id="what-is-the-job-that-your-docs-need-to-do"&gt;What is the job that your docs need to do?&lt;/h1&gt;&lt;p&gt;Two threads have been running through my mind recently, and I keep finding connections between them that I can't shake. The first is Diataxis - a structured framework for documentation that divides all docs into four distinct types: tutorials, how-to guides, reference, and explanation. The second is Clayton Christensen's jobs theory, which asks a deceptively simple question: what is the job that your customer needs to get done?&lt;/p&gt;
&lt;p&gt;Side note: I've been heavily inspired by Clayton Christensen's books recently, and have audiobooked my way through Innovator's Dilemma/Solution/DNA, as well as Competing Against Luck. All good books, 100% recommended if you're interested in understanding how innovation actually works.&lt;/p&gt;
&lt;p&gt;Here's the key insight: your documentation isn't competing with other documentation. It's competing with every other way someone could accomplish their job.&lt;/p&gt;
&lt;h2 id="the-competition-you-didn-t-know-you-had"&gt;The competition you didn't know you had&lt;/h2&gt;&lt;p&gt;When someone opens your internal documentation, they're looking for more than information. They're trying to accomplish something specific, and they're evaluating whether your docs are the right tool for that job.&lt;/p&gt;
&lt;p&gt;For internal company documentation—whether it's for internally built software, processes, or systems—the competition is different but equally real. Your how-to guide competes with asking a colleague, digging through Slack history, or reverse-engineering from existing code. Your reference docs compete with reading the source code directly, checking configuration files, or experimenting in a staging environment.&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Diataxis Doc Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Jobs to be Done (JTBD)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Alternative Product Categories That Could Be Hired&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How-to Guides&lt;/td&gt;
&lt;td&gt;"Show me how to achieve a specific outcome."&lt;/td&gt;
&lt;td&gt;Asking a colleague, Slack/Teams search, reverse-engineering from existing code, trial and error in staging&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reference&lt;/td&gt;
&lt;td&gt;"Give me exact technical information I can look up quickly."&lt;/td&gt;
&lt;td&gt;Reading source code, checking config files, database schemas, API endpoint testing, environment variables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explanation&lt;/td&gt;
&lt;td&gt;"Help me understand how/why it works."&lt;/td&gt;
&lt;td&gt;Architecture diagrams, code comments, git history, team knowledge sharing sessions, design documents&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tutorials&lt;/td&gt;
&lt;td&gt;"Help me learn by doing, in a safe structured way."&lt;/td&gt;
&lt;td&gt;Pair programming, shadowing a colleague, sandbox environments, local development setup walkthroughs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Once you see this competition, it reframes how you think about documentation structure.&lt;/p&gt;
&lt;h2 id="the-opportunity-hiding-in-plain-sight"&gt;The opportunity hiding in plain sight&lt;/h2&gt;&lt;p&gt;Here's what's fascinating about internal documentation: the competition is actually pretty terrible. Think about it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Asking a colleague interrupts their work and creates context switching for both of you&lt;/li&gt;
&lt;li&gt;Digging through Slack history is time-consuming and often incomplete&lt;/li&gt;
&lt;li&gt;Reverse-engineering from existing code is slow and error-prone&lt;/li&gt;
&lt;li&gt;Trial and error in staging environments wastes time and resources&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means that even moderately good internal documentation has a much lower bar to clear than external documentation. Your internal how-to guide doesn't need to compete with polished YouTube tutorials - it just needs to be better than interrupting Sarah from accounting or spending 30 minutes searching through #engineering-general.&lt;/p&gt;
&lt;p&gt;This is actually a huge opportunity. While external documentation faces fierce competition from Stack Overflow's crowdsourced answers and professionally produced tutorials, internal documentation often competes with... nothing systematic at all.&lt;/p&gt;
&lt;p&gt;The result? Even basic improvements to internal documentation can have outsized impact on team productivity. When your reference docs are faster than reading source code, people will use them. When your how-to guides are clearer than tribal knowledge, they become the default choice.&lt;/p&gt;
&lt;h2 id="understanding-the-competition"&gt;Understanding the competition&lt;/h2&gt;&lt;p&gt;Most documentation is written from the perspective of the product being documented. It's organized around features, capabilities, and technical architecture. But when you flip the perspective to focus on jobs-to-be-done, you can structure information more effectively around what readers actually need to accomplish.&lt;/p&gt;
&lt;h2 id="a-job-focused-approach-in-practice"&gt;A job-focused approach in practice&lt;/h2&gt;&lt;p&gt;Let me show you what this looks like in practice. Say you're writing a how-to guide for deploying your company's internal microservice. The traditional approach focuses on what information to include. The job-focused approach starts with the specific outcome: "Help me get this service deployed so I can test my feature and merge my PR."&lt;/p&gt;
&lt;p&gt;That job-focused lens shifts how you structure the guide:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You lead with the most common use case and a working example first, then dive into edge cases&lt;/li&gt;
&lt;li&gt;You include troubleshooting steps for the most common failure modes&lt;/li&gt;
&lt;li&gt;You assume they're in a hurry and want to get back to their main project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every decision gets filtered through the lens of "does this help the reader accomplish their job better?"&lt;/p&gt;
&lt;p&gt;The result? Documentation that people actually use because it's genuinely better at helping them accomplish their jobs.&lt;/p&gt;
&lt;h2 id="how-to-apply-this-framework"&gt;How to apply this framework&lt;/h2&gt;&lt;p&gt;Start by identifying the specific job your reader is trying to accomplish. Not the general topic area, but the specific outcome they need to achieve. Then ask yourself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;What alternatives could they hire instead of your documentation?&lt;/li&gt;
&lt;li&gt;What unique value can your documentation provide that those alternatives can't?&lt;/li&gt;
&lt;li&gt;How can you structure the information to make their job easier to accomplish?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For that third question especially, consider using AI to help you think through different structural approaches. You can prompt an AI with the specific job your reader needs to accomplish and ask it to suggest multiple ways to organize the information, then choose the approach that best serves that job.&lt;/p&gt;
&lt;p&gt;Take reference documentation as an example. The job goes beyond providing comprehensive information about all internal API endpoints or configuration options. The real job is "give me exact technical information I can look up quickly." This means your reference docs need to be faster and more precise than someone could get from reading your source code, checking configuration files, or asking in Slack.&lt;/p&gt;
&lt;p&gt;If someone can figure out what they need faster by just reading the source code or asking a colleague, your reference docs aren't doing their job.&lt;/p&gt;
&lt;h2 id="how-this-applies-to-ai-assisted-documentation"&gt;How this applies to AI-assisted documentation&lt;/h2&gt;&lt;p&gt;Here's where this gets really interesting. When documentation is designed around jobs-to-be-done, it creates a positive feedback loop that extends beyond the direct reader.&lt;/p&gt;
&lt;p&gt;Well-structured, job-focused documentation helps humans and also helps AI systems understand context and provide better assistance to future users. When your how-to guide is crystal clear about the specific outcome it helps achieve, an AI can better understand when to recommend that guide to someone with a similar job.&lt;/p&gt;
&lt;p&gt;The result is that good documentation becomes a force multiplier. Beyond helping the direct reader, it helps AI systems help other readers accomplish similar jobs faster and more accurately.&lt;/p&gt;
&lt;p&gt;This creates a flywheel effect: better documentation helps more people accomplish their jobs, which generates more usage data and feedback, which leads to even better documentation that helps both humans and AI serve users more effectively.&lt;/p&gt;
&lt;h2 id="applying-this-perspective"&gt;Applying this perspective&lt;/h2&gt;&lt;p&gt;The next time you write internal documentation, start with what job your reader is trying to accomplish rather than what you want to explain.&lt;/p&gt;
&lt;p&gt;Ask yourself: if someone could accomplish this job faster or more reliably using a different approach, why would they choose your documentation instead? For internal docs, this question often has a surprising answer: because the alternatives are genuinely worse.&lt;/p&gt;
&lt;p&gt;This is liberating. Your internal documentation doesn't need to be perfect - it just needs to be better than the current chaos of tribal knowledge and ad-hoc problem-solving.&lt;/p&gt;
&lt;p&gt;When you can answer that question clearly, you'll write documentation that people find genuinely useful. And when people use your documentation successfully, they become more successful with your product.&lt;/p&gt;
&lt;p&gt;This matters because documentation is ultimately about scaling our collective knowledge and decision-making capacity. But that scaling only happens when people actually use the documentation. And people only use documentation when it helps them accomplish specific jobs they need to get done.&lt;/p&gt;
&lt;p&gt;For internal documentation, this scaling opportunity is especially significant. Every time someone uses your docs instead of interrupting a colleague, you're not just solving one person's problem - you're preserving focus and momentum across your entire team.&lt;/p&gt;
&lt;p&gt;That's the value of thinking about internal documentation as a product designed around jobs-to-be-done. It creates a better experience for everyone who interacts with your work, and unlike external documentation, you don't need to beat world-class competition to succeed.&lt;/p&gt;
</content></entry><entry><title>One hour and eight minutes: Building a receipt scanner with the weirdest tech stack imaginable</title><link href="https://ericmjl.github.io/blog/2025/7/1/one-hour-and-eight-minutes-building-a-receipt-scanner-with-the-weirdest-tech-stack-imaginable/" rel="alternate"/><updated>2025-07-01T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:a195e873-9317-3fef-b7e0-a0c3c634425f</id><content type="html">&lt;p&gt;After bouncing between Cursor and GitHub Copilot for the past couple of years, I kept hearing about Claude Code. People's experiences were really piquing my curiosity, so I decided to give it a shot. What happened next completely changed how I think about rapid prototyping.&lt;/p&gt;
&lt;p&gt;I built a fully functional receipt scanning and expense tracking app in exactly one hour and eight minutes. But here's the kicker—I used a technology stack so unconventional that most developers would probably laugh at me. And it worked beautifully.&lt;/p&gt;
&lt;p&gt;Let me tell you what I learned about the immersive power of terminal-based development and why weird tech combinations might be the secret to lightning-fast tool building.&lt;/p&gt;
&lt;h2 id="the-problem-i-wanted-to-solve"&gt;The problem I wanted to solve&lt;/h2&gt;&lt;p&gt;At work, I noticed SAP Concur can automatically extract fields from uploaded receipts. I thought, "What if I could replicate that at home?" I wanted to track my expenses without paying for QuickBooks, using Notion as my database instead.&lt;/p&gt;
&lt;p&gt;Most developers would reach for the standard stack: React frontend, PostgreSQL backend, maybe throw in some Express.js. That's the sensible approach.&lt;/p&gt;
&lt;p&gt;But I'm not building production software for thousands of users. I'm a data scientist experimenting with tools for myself. So I decided to get weird with it.&lt;/p&gt;
&lt;h2 id="the-stack-that-shouldn-t-work-but-does"&gt;The stack that shouldn't work but does&lt;/h2&gt;&lt;p&gt;Here's what Claude Code helped me build with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;FastAPI&lt;/strong&gt; for the backend (this part makes sense)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTMX&lt;/strong&gt; for the frontend instead of React (getting unusual)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vanilla HTML/CSS&lt;/strong&gt; with minimal JavaScript (now we're talking)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LlamaBot&lt;/strong&gt; for AI interactions (I made it, so I know it works)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Notion&lt;/strong&gt; as the database (yes, you read that right)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If I were to describe this stack to a seasoned developer, they'd probably be surprised, then laugh out loud, and then go "what?" But when I described it to Claude Code and specified that I wanted everything in a single &lt;code&gt;app.py&lt;/code&gt; file that I could run with &lt;code&gt;uv run app.py&lt;/code&gt;, Claude Code got creative.&lt;/p&gt;
&lt;p&gt;It generated a beautiful single-file application with PEP 723 metadata at the top. The code was clean and well-structured. It took a few iterations of AI-generated code writing followed by testing, but it was always generally headed in the right direction. And this is the result:&lt;/p&gt;
&lt;p&gt;&lt;img src="screenshot.webp" alt=""&gt;&lt;/p&gt;
&lt;h2 id="the-development-experience-that-changed-everything"&gt;The development experience that changed everything&lt;/h2&gt;&lt;p&gt;Here's what blew my mind about using Claude Code: the immersive experience.&lt;/p&gt;
&lt;p&gt;I spent the entire development session in just two terminal tabs. One tab running Claude Code, another tab with my &lt;code&gt;uvicorn&lt;/code&gt; server running with auto-reload. That's it. No switching between file explorers, no hunting through directory structures, no context switching between different applications.&lt;/p&gt;
&lt;p&gt;I was in what I can only describe as "vibe-ish coding" mode—not quite the &lt;a href="https://simonwillison.net/2025/Mar/19/vibe-coding/"&gt;vibe coding that Simon Willison describes&lt;/a&gt;, but close. I'd type a request to Claude Code, see the changes instantly in my browser, then iterate. The feedback loop was immediate and distraction-free.&lt;/p&gt;
&lt;p&gt;This terminal-focused workflow kept me in the zone in a way that traditional IDEs never have. Without all the little icons, bells, and whistles that can distract you in an IDE, I could maintain focus on the actual problem I was solving instead of fighting with tools.&lt;/p&gt;
&lt;h2 id="what-got-built-in-68-minutes"&gt;What got built in 68 minutes&lt;/h2&gt;&lt;p&gt;By the time my terminal session ended, I had a fully functional application that could:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Upload single or multiple receipt images&lt;/li&gt;
&lt;li&gt;Extract expense data using LlamaBot's AI capabilities&lt;/li&gt;
&lt;li&gt;Allow manual editing of fields that the AI got wrong (inside Notion)&lt;/li&gt;
&lt;li&gt;Handle enumerated types for expense categories&lt;/li&gt;
&lt;li&gt;Automatically populate a Notion database with extracted data&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The AI integration was seamless. I provided my OpenAI API key, and through LlamaBot I was able to hit the OpenAI API while Claude Code handled all the integration complexity. When I needed to add file upload functionality, I pasted some Notion API documentation as context, and Claude Code implemented it correctly.&lt;/p&gt;
&lt;p&gt;The end result? I can now drag and drop receipts into a web interface, hit submit, and watch the data appear automatically in my Notion expense tracker. Exactly what I wanted.&lt;/p&gt;
&lt;h2 id="pushing-language-models-to-their-limits"&gt;Pushing language models to their limits&lt;/h2&gt;&lt;p&gt;Here's the thing that really fascinated me about this experiment: I deliberately chose this weird tech stack to test Claude Code's boundaries.&lt;/p&gt;
&lt;p&gt;Think about it—if I had gone with React, Node.js, and PostgreSQL, that would be easy for any language model. Those patterns show up constantly in training data.&lt;/p&gt;
&lt;p&gt;But I wanted to push to the edges. What happens when you combine technologies that people don't usually think about together? HTMX with FastAPI? Notion as a database backend? A single-file Python app doing receipt processing with AI?&lt;/p&gt;
&lt;p&gt;This is uncharted territory for most language models. There aren't thousands of tutorials showing how to integrate LlamaBot with HTMX forms, or how to structure FastAPI routes that return HTML fragments for dynamic updates.&lt;/p&gt;
&lt;p&gt;Yet Claude Code handled it beautifully. It figured out how to make these disparate pieces work together, even when the combination got weird.&lt;/p&gt;
&lt;h2 id="why-this-matters-for-tool-building"&gt;Why this matters for tool building&lt;/h2&gt;&lt;p&gt;This experience reinforced something I've been thinking about lately: the best time to build custom tools is right now, and the barrier to entry has never been lower.&lt;/p&gt;
&lt;p&gt;I wrote about this recently in my post on building your own tools with AI coding assistants. If you need a tool, just build it. Don't wait for the perfect stack or the right framework. Pick technologies that let you move fast and iterate quickly.&lt;/p&gt;
&lt;p&gt;The ability to combine unusual technologies successfully opens up new possibilities. Instead of being constrained by conventional wisdom about what technologies "should" work together, you can experiment with combinations that solve your specific problem elegantly.&lt;/p&gt;
&lt;h2 id="the-immersive-development-advantage"&gt;The immersive development advantage&lt;/h2&gt;&lt;p&gt;The most valuable lesson from this experiment wasn't about technology—it was about workflow.&lt;/p&gt;
&lt;p&gt;Claude Code's terminal-based approach created an immersive development environment that kept me focused. No file system distractions, no IDE complexity, just pure problem-solving in a clean interface.&lt;/p&gt;
&lt;p&gt;This suggests that tool choice matters more than we often acknowledge. The best coding assistant isn't necessarily the one with the most features—it's the one that keeps you in flow state while you build.&lt;/p&gt;
&lt;h2 id="what-s-next"&gt;What's next&lt;/h2&gt;&lt;p&gt;I'm already planning my next experiment with Claude Code. Maybe a document processing pipeline using Docling, Anthropic's API, and Airtable. Or a personal CRM built with FastAPI, HTMX, and Google Sheets as the backend.&lt;/p&gt;
&lt;p&gt;The point isn't to build production-ready applications with these stacks. It's to explore what becomes possible when you remove the friction from experimentation.&lt;/p&gt;
&lt;p&gt;In an hour and eight minutes, I went from idea to working application. That's the kind of development velocity that changes what you're willing to attempt.&lt;/p&gt;
&lt;p&gt;Sometimes the weirdest combinations turn out to be exactly what you need.&lt;/p&gt;
</content></entry><entry><title>Build your own tools!</title><link href="https://ericmjl.github.io/blog/2025/6/27/build-your-own-tools/" rel="alternate"/><updated>2025-06-27T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:bdee977f-1cea-30bd-8d27-2c5c3cf1032a</id><content type="html">&lt;p&gt;On 25 June 2025, I delivered a talk at Data-Driven Pharma, an event organized by &lt;a href="https://www.linkedin.com/in/dricaptain/"&gt;Ilya Captain&lt;/a&gt; and the namesake Data-Driven Pharma organization. In the run-up to the talk, I had been reflecting on two points:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;I hate making slides, and&lt;/li&gt;
&lt;li&gt;I really love building tools.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;To that end, I decided... well, I'm not going to bother with making slides. And I'll build a tool that makes slides for me instead. Hence [DeckBot], which currently lives in a marimo notebook, was born. I started off by telling the crowd how much I hated making slides:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;In an age of LLMs and plain .txt, I understand why I have such a disdain for powerpoint: you can't easily automate their creation, there's too much that can be hidden behind a bullet point, and it's just an all-round ineffective media for &lt;em&gt;lasting&lt;/em&gt; crystal clear communication. By contrast, Markdown slides are better.&lt;/p&gt;
&lt;p&gt;-- Original post link &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7335296923488194561?trk=public_post_embed_social-actions-reactions"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And how even Andrej Karpathy laments the absence of an LLM-enabled tool for building slides:&lt;/p&gt;
&lt;p&gt;&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;Making slides manually feels especially painful now that you know Cursor for slides should exist but doesn’t.&lt;/p&gt;&amp;mdash; Andrej Karpathy (@karpathy) &lt;a href="https://twitter.com/karpathy/status/1931042840966222046?ref_src=twsrc%5Etfw"&gt;June 6, 2025&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;&lt;/p&gt;
&lt;p&gt;Also, my informal poll of the audience revealed that approximately 2/3 of the crowd also hated making slides. Not surprising!&lt;/p&gt;
&lt;p&gt;So I decided to take that as a nerdsnipe and actually make DeckBot. After showing the audience (live!) how I can make rando slides for completely nondescript topics, such as, "Why eating well is so important" or "pros and cons of buying a thing", I then proceeded with the real exciting challenge of this talk: to get an LLM to generate my entire slide deck for the actual topic I wanted to talk about, from which I would present. And that topic was, well, "Build your own tools!". I then proceeded to copy/paste in the first draft of this blog post into the notebook, and 1 minute later, I had my slides, from which I presented live.&lt;/p&gt;
&lt;p&gt;Below is a writeup of what I actually presented, including a written description of some of the interactions.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;My main message to everybody today is this: If you're a data scientist, computational biologist, or software developer, you should learn how to build your own tools. Building your own tools is a liberating endeavor. It injects joy back into your day-to-day work. People were made to be creative creators. Build your own tools.&lt;/p&gt;
&lt;h2 id="a-flashback-from-my-grad-school-days"&gt;A flashback from my grad school days&lt;/h2&gt;&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/images/circos.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Do you know what this diagram is? The audience came in clutch, many people knew what this was -- it's a Circos plot. Some may have seen it with arcs rather than dots around the edges, but the concept remains the same: prioritize ordering nodes and then draw in the edges.&lt;/p&gt;
&lt;p&gt;I wanted to learn how to make a graph visualization like this. But the only tool I saw out there was written in a different language (Perl), had no Python bindings, and was way too complicated for me—a beginner programmer in 2014—to learn. So I decided to leverage two other tools that I knew at the time, Python and matplotlib, to make my own Python package, both to learn software development and to understand the principles of rational network visualization.&lt;/p&gt;
&lt;p&gt;The precursor to nxviz, &lt;code&gt;circosplot&lt;/code&gt;, was born in 2015. One year later, I knew enough to make all sorts of network visualizations!&lt;/p&gt;
&lt;p&gt;Like this, the matrix plot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/examples/matrix/output_4_0.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or this, a geo plot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/examples/geo/output_6_1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or this, an arc plot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/examples/arc_node_labels/output_2_1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or this, another circos plot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/examples/circos_node_labels/output_3_0.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Or this beautiful thing, a hive plot:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/api/high-level-api/output_12_0.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;What's the unifying thread behind all of those plots? As it turns out, the thing I learned while building my own graph visualization tool was that &lt;strong&gt;rational and beautiful graph visualization starts with knowing how to order &lt;em&gt;nodes&lt;/em&gt; in a graph, and then drawing in the edges&lt;/strong&gt;. I would have never learned that had I not attempted to reinvent the wheel (or, perhaps, Circos plots)! Additionally, being able to build my own Python package was superbly empowering, especially as a graduate student! I could build my own tools, archive them in the public domain, and never have to solve the same problem again. This echoed Simon Willison's approach to software development:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I realized that one of the best things about open source software is that you can solve a problem once and then you can slap an open source license on that solution and you will &lt;em&gt;never&lt;/em&gt; have to solve that problem ever again, no matter who's employing you in the future.&lt;/p&gt;
&lt;p&gt;It's a sneaky way of solving a problem permanently.&lt;/p&gt;
&lt;p&gt;-- Original post link by Simon Willison &lt;a href="https://simonwillison.net/2025/Jan/24/selfish-open-source/"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;If I didn't know how to build my own tools, I'd have been stuck, and I'd never have learned anything new.&lt;/p&gt;
&lt;h2 id="fast-forward-to-2018-at-novartis"&gt;Fast-forward to 2018 at Novartis&lt;/h2&gt;&lt;p&gt;My colleague Brant Peterson showed me the R package &lt;code&gt;janitor&lt;/code&gt;, and I thought, "Why can't Pythonistas have nice things?"&lt;/p&gt;
&lt;p&gt;Then, I remembered Gandhi's admonition&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"Be the change you wish to see in the world."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And so, &lt;code&gt;pyjanitor&lt;/code&gt; was born.&lt;/p&gt;
&lt;p&gt;Your dataframe manipulation and processing code can now be more expressive than native pandas:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DataFrame&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;company_sales&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove_columns&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Company1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dropna&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subset&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Company2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Company3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Company2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Amazon&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rename_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Company3&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Facebook&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_column&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Google&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;450.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;550.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;800.0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;By being the change I wanted to see, Pythonistas now have one more nice thing available to them.&lt;/p&gt;
&lt;p&gt;And of course, I just &lt;em&gt;had&lt;/em&gt; to inject this in: that was all in 2018.&lt;/p&gt;
&lt;p&gt;It's now 2025. Use polars. :)&lt;/p&gt;
&lt;h2 id="building-resilience-at-moderna"&gt;Building resilience at Moderna&lt;/h2&gt;&lt;p&gt;Fast-forward to 2021. I joined Moderna, attracted by the forward-thinking Digital leadership and their suite of high-power home-grown tools. It was a dog-fooding culture back then—one I've fought hard to keep alive within the Digital organization.&lt;/p&gt;
&lt;p&gt;Since I was only data scientist #6 at Moderna and was hired into a relatively senior role (Principal Data Scientist), I saw the chance to set standards for Moderna data scientists.&lt;/p&gt;
&lt;p&gt;Together with my wonderful colleague &lt;a href="https://www.linkedin.com/in/adriannaloback/"&gt;Adrianna Loback&lt;/a&gt; and our manager &lt;a href="https://www.linkedin.com/in/giessel/"&gt;Andrew Giessel&lt;/a&gt;, we hammered out what Data Scientists would ship: dockerized CLI tools run in the cloud, and Python packages, and designed our entire project initialization workflow around deploying those two things. As time progressed, the tooling evolved, and &lt;a href="https://www.linkedin.com/in/dandluu/"&gt;Dan Luu&lt;/a&gt; helped us be a caretaker of the tooling as well, continually improving it and modernizing it.&lt;/p&gt;
&lt;p&gt;By standardizing on what we ship and then standardizing on the toolchain, we implemented a design pattern that made it easy for us to help one another. I can jump into a colleague's codebase dealing with Clinical Development and be helpful in a modestly short amount of time, even when I mostly work on Research projects.&lt;/p&gt;
&lt;p&gt;And here's a side effect: we designed a portable way of working that works best when you give a Moderna data scientist access to a raw Linux machine. As Andrew Giessel once mentioned to me:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Eventually, tools that abstract away the Linux operating system will fail to satisfy users as they grow up and master Linux. They'll want to jump out of a container and just run raw Linux. Anything that tries to abstract away the filesystem, shell scripts, and more eventually runs into edge cases, so why not just give people access to a raw Linux machine with tools pre-installed?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As it turns out, this evening's other presenter &lt;a href="https://www.linkedin.com/in/%F0%9F%8E%AF-ming-tommy-tang-40650014/"&gt;Tommy Tang&lt;/a&gt; is also a big fan of the shell:&lt;/p&gt;
&lt;iframe src="https://www.linkedin.com/embed/feed/update/urn:li:share:7341052068264128513?collapsed=0" height="265" width="504" frameborder="0" allowfullscreen="" title="Embedded post"&gt;&lt;/iframe&gt;&lt;p&gt;So now I'm a big fan of giving people access to a raw Linux box, outside of a sandboxed container. Being able to build and run a container is a fundamental skill nowadays—so much so that as a community of data scientists, we've effectively said "no" to vendor tooling that forces us to do our day-to-day work within a Docker container.&lt;/p&gt;
&lt;p&gt;And here's the most awesome part: we did this in an "internally open source" fashion. &lt;em&gt;Anyone&lt;/em&gt; with a complaint about the tooling can propose a fix to our tools. Even better, we'll walk you through making the fix "the right way," so you gain the superpower of software development along the way!&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/3ZTGwcHQfLY?si=_FLzvFyCp88ZlzGm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;p&gt;At least on this dimension, we are never beholden to someone else's (or a vendor's) roadmap! We are now &lt;em&gt;resilient&lt;/em&gt;—just like Dustin from Smarter Every Day described when he made this video about trying to make things "in America."&lt;/p&gt;
&lt;p&gt;I'll end this section with a huge lesson I've learned during my time working here:&lt;/p&gt;
&lt;iframe src="https://www.linkedin.com/embed/feed/update/urn:li:share:7337223460651220992" height="349" width="504" frameborder="0" allowfullscreen="" title="Embedded post"&gt;&lt;/iframe&gt;&lt;h2 id="building-teaches-you-the-domain"&gt;Building teaches you the domain&lt;/h2&gt;&lt;p&gt;Do you remember these beautiful graph diagrams from earlier?&lt;/p&gt;
&lt;p&gt;&lt;img src="https://ericmjl.github.io/nxviz/examples/arc_node_labels/output_2_1.png" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Building is a great way to learn new things. Building nxviz helped me learn the principles of graph visualization. Building LlamaBot helped me learn about making LLM applications.&lt;/p&gt;
&lt;p&gt;In 2023, I created LlamaBot because I was confused about how to interact with and build LLMs, particularly RAG applications. I decided to turn to my favorite learning tool: building software. This was clarifying—I was forced to encode my understanding into code, and if the code did unexpected things, I knew my understanding was wrong. After all:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Computers are the best students there are. If you teach the computer something wrong, it'll give you back wrong answers. If you design things wrongly, this student will make life hard for you. So you learn to get good at verification.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I've rewritten LlamaBot at least 4 times, each time updating the codebase with the best of my knowledge. Each time round, my understanding improved, and the abstractions changed along with them, and the ergonomics of using LlamaBot got better, more natural. Throughout the changes, some things that have stayed constant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The "Bot" analogy, which predates the term "agents," turns out to be a natural way to express Agents.&lt;/li&gt;
&lt;li&gt;The docstore abstraction simplifies storage and retrieval for pure text applications.&lt;/li&gt;
&lt;li&gt;My distaste for writing commit messages and release notes—hence the automated writers for both remain deeply ingrained as dog-fooded tools.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Some things that have evolved:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;QueryBot used to do entire RAG workflows all-in-one—from PDF-to-text conversion to embedding to retrieval. I've since learned it's much better to break those out into separate steps.&lt;/li&gt;
&lt;li&gt;ChatBot used to have a built-in ChatUI. I dropped it because it was too opinionated and unwieldy. Marimo has really good chat UI primitives that should be used instead.&lt;/li&gt;
&lt;li&gt;Inspiration from the &lt;code&gt;ell&lt;/code&gt; library: &lt;code&gt;lmb.user("some prompt")&lt;/code&gt; or &lt;code&gt;lmb.system("some prompt")&lt;/code&gt; for convenient creation of system and user prompts.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In the process of building and designing software, we have to learn the domain so well that we become linguistics experts in that domain. Vocabulary, terms, and their relationships become natural extensions of what we already know. If our code maps to the domain properly, our abstractions become so natural they're self-documenting. If our code maps poorly onto a solid understanding of the problem space, it'll end up being a tangled mess that warrants a rewrite. There's nothing wrong with that! Embrace the need to rewrite—with AI assistance nowadays, the activation energy barriers to building your own tools is dramatically reduced.&lt;/p&gt;
&lt;h2 id="internal-tooling-requires-organizational-buy-in"&gt;Internal tooling requires organizational buy-in&lt;/h2&gt;&lt;p&gt;I then made my next point: you want to make sure you have organizational buy-in to any tool building efforts. It's super telling if your line management doesn't agree with you. On the other hand, it's super awesome if someone is going to be hired explicitly for tooling, like at Quora below:&lt;/p&gt;
&lt;p&gt;&lt;blockquote class="twitter-tweet"&gt;&lt;p lang="en" dir="ltr"&gt;We are opening up a new role at Quora: a single engineer who will use AI to automate manual work across the company and increase employee productivity. I will work closely with this person. &lt;a href="https://t.co/iKurWS6W7v"&gt;pic.twitter.com/iKurWS6W7v&lt;/a&gt;&lt;/p&gt;&amp;mdash; Adam D&amp;#39;Angelo (@adamdangelo) &lt;a href="https://twitter.com/adamdangelo/status/1936504553916309617?ref_src=twsrc%5Etfw"&gt;June 21, 2025&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src="https://platform.twitter.com/widgets.js" charset="utf-8"&gt;&lt;/script&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://pbs.twimg.com/media/Gt_VT5nakAANTdj?format=png&amp;amp;name=large" alt=""&gt;&lt;/p&gt;
&lt;p&gt;Reading this tweet triggered a thought in my mind: sustaining internal tool builds help with organizational buy-in. Does your organization empower you to build the tools you need to get your work done? I was lucky to have full leadership buy-in through Andrew Giessel and Dave Johnson, and my current manager Wade keeps roadblocks away from innovating on how we work. I also try to encourage this across teams I have influence with, even without direct managerial responsibilities.&lt;/p&gt;
&lt;p&gt;But as I also mentioned earlier, even though sustaining an internal tool build can be boosted with organizational buy-in, &lt;em&gt;culture needs no permission&lt;/em&gt;. We always have agency. We always have the free will to make things happen. We always can go forth and build. Build the smallest thing that gets roadblocks out of your way and move on. Throwaway builds are OK! No permission required.&lt;/p&gt;
&lt;h2 id="expert-practitioners-agree-build-your-own-tools"&gt;Expert practitioners agree: build your own tools&lt;/h2&gt;&lt;p&gt;If my arguments don't convince you, perhaps Hamel Husain, one of the leading AI eval practitioners, will:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;&lt;strong&gt;Build a custom annotation tool.&lt;/strong&gt; This is the single most impactful investment you can make for your AI evaluation workflow. With AI-assisted development tools like Cursor or Lovable, you can build a tailored interface in hours. I often find that teams with custom annotation tools iterate ~10x faster.&lt;/p&gt;
&lt;p&gt;Custom tools excel because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They show all your context from multiple systems in one place&lt;/li&gt;
&lt;li&gt;They can render your data in a product-specific way (images, widgets, markdown, buttons, etc.)&lt;/li&gt;
&lt;li&gt;They're designed for your specific workflow (custom filters, sorting, progress bars, etc.)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Off-the-shelf tools may be justified when you need to coordinate dozens of distributed annotators with enterprise access controls. Even then, many teams find the configuration overhead and limitations aren't worth it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;He makes a great point: "With AI-assisted development tools like Cursor or Lovable, you can build a tailored interface in hours."&lt;/p&gt;
&lt;p&gt;The barrier to entry for building your own tools nowadays is so much lower than before. Much of the grunt work can be automated away using templating and LLM assistance. If you want to build, now is the time to build.&lt;/p&gt;
&lt;h2 id="software-development-scales-everything"&gt;Software development scales everything&lt;/h2&gt;&lt;p&gt;I love the work I do partly because it is in the service of the discovery of medicines, and partly because I have an outlet for expressing creativity through the tools I make for myself and others. Through nearly 10 years of making tools, I've crystallized this lesson in scaling:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Software scales our labor.&lt;/li&gt;
&lt;li&gt;Documentation scales our brains.&lt;/li&gt;
&lt;li&gt;Tests scale others' trust in our code.&lt;/li&gt;
&lt;li&gt;Design scales our agility.&lt;/li&gt;
&lt;li&gt;Agents scale our processes.&lt;/li&gt;
&lt;li&gt;Open source scales opportunity for impact.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If you can build software tools for yourself, you can scale yourself. If you teach others to use those same tools, you can scale their labor. You can scale your brain by documenting those tools well. If you test those tools thoroughly, you can scale trust in the codebase, enabling others to contribute with confidence. If you design the software well—and more importantly, design the business process that software supports well—you can become nimble and agile without the trappings of Big Fake Agile. If you use agents, and more generally automation as part of the custom tooling, you can scale those same processes even further. If you make your tooling open source (whether internally or externally), you scale the opportunity for others to contribute.&lt;/p&gt;
&lt;p&gt;Culture needs no permission (another great lesson that I learned from Andrew Giessel), and if you need to unblock yourself, build your own tools. There is no magic sauce in the choice of tools that we use and make. &lt;strong&gt;The magic sauce is in the people who choose to show up and build.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;And so, my fellow builders, let's build. Not because your company wants it of you, but because patients are waiting. Patients have no patience. We joined this line of work because we want to have the greatest impact on patients with our medicines. Computational types like should never be the bottleneck to shipping medicines. Building tools for ourselves empowers us to keep ourselves unstuck, remove the viscous traps that slow you down, and keep medicines moving.&lt;/p&gt;
&lt;p&gt;I'll now leave you with a final quote, from Michael Jackson's song, Heal the World:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;There are people dying, if you care enough for the living, make a better place for you and for me.&lt;/p&gt;
&lt;p&gt;— Heal the World (Michael Jackson)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And so to my fellow techies in bio, it's time to build. Thank you.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="reactions"&gt;Reactions&lt;/h2&gt;&lt;p&gt;After Tommy's talk, we had another round of networking, which was awesome. I heard some great perspectives. &lt;a href="https://www.linkedin.com/in/kucukural/"&gt;Alper Kucukural&lt;/a&gt;, who is both an industry and academia person, mentioned how his students needed to hear the message that they can be empowered to build their own tools, no permission required. Too many get stuck. Students -- learn how to build!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/maciejpacula/"&gt;Maciej Pacula&lt;/a&gt; also posted his reaction on LinkedIn:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;I had a great time at the &lt;a href="https://www.linkedin.com/company/datadrivenpharma/"&gt;DataDrivenPharma&lt;/a&gt; event at Moderna yesterday. Thanks &lt;a href="https://www.linkedin.com/in/ACoAAAd30LUB7VdWv0AHDEMM72Cm0cyIZgSNc_4"&gt;&lt;/a&gt;&lt;a href="https://www.linkedin.com/in/dricaptain/"&gt;Ilya Captain, PhD&lt;/a&gt; for organizing, and hope you bring more such events to the East Coast!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ericmjl/"&gt;Eric Ma&lt;/a&gt;'s talk about building your own tools and using them as a force multiplier not just for yourself but for others resonated deeply.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/%F0%9F%8E%AF-ming-tommy-tang-40650014/"&gt;🎯 Ming "Tommy" Tang&lt;/a&gt;'s talk about "good enough" reproducibility made the excellent point that sometimes you just need to talk to the lab scientists (what a concept!) and collaborate on common standards.&lt;/p&gt;
&lt;p&gt;Appreciated the shout out for &lt;a href="https://www.linkedin.com/company/gofigr/"&gt;GoFigr.io&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/%F0%9F%8E%AF-ming-tommy-tang-40650014/"&gt;🎯 Ming "Tommy" Tang&lt;/a&gt; :-)&lt;/p&gt;
&lt;p&gt;Thanks &lt;a href="https://www.linkedin.com/in/ted-natoli-compbio/"&gt;Ted Natoli&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/colles/"&gt;Colles Price M.S., Ph.D.&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/wesserg/"&gt;Sergiusz Wesolowski&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/ilyashl/"&gt;Ilya Shlyakhter&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/vasant-marur/"&gt;Vasant Marur&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/kucukural/"&gt;Alper Kucukural, PhD&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/jamesjcrowley/"&gt;James J. Crowley&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/gunjan-singh-thakur-b8251620/"&gt;Gunjan Singh Thakur&lt;/a&gt; for the company and conversation.&lt;/p&gt;
&lt;p&gt;-- Original post link &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7344004044933226496/"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ericmerle/"&gt;Eric Merle&lt;/a&gt;'s reaction is below:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A lot is possible when we build the right tools...&lt;/p&gt;
&lt;p&gt;Yesterday's DataDrivenPharma event at &lt;a href="https://www.linkedin.com/company/modernatx/"&gt;Moderna&lt;/a&gt; completely energized my thinking about exactly that and I'll tell you specifically why.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://www.linkedin.com/in/ericmjl/"&gt;Eric Ma&lt;/a&gt; from Moderna shared something that hit home: "Data scientists should never become bottlenecks in getting medicines to patients who need them." His approach to building custom tools that scale impact, from automated slide generation to standardized project workflows, showed exactly how thoughtful tooling can accelerate discovery.
&lt;a href="https://www.linkedin.com/in/%F0%9F%8E%AF-ming-tommy-tang-40650014/"&gt;🎯 Ming "Tommy" Tang&lt;/a&gt; from &lt;a href="https://www.linkedin.com/company/astrazeneca/"&gt;AstraZeneca&lt;/a&gt; complemented this perfectly with his presentation on reproducible bioinformatics practices. His insights on proper file naming conventions (how many of us are guilty of having final1, final2, final3 files?), consistent folder structures, and creating reproducible workflows provided the foundation that makes scaling actually possible. You can't build lasting tools without these fundamentals in place.&lt;/p&gt;
&lt;p&gt;Both emphasized that that it's not just writing code, but also about building infrastructure. Eric's philosophy around scaling through software combined with Tommy's disciplined approach to reproducibility showed how the right practices can create tools that continue delivering value long after the original builder moves on.&lt;/p&gt;
&lt;p&gt;The potential to create AI tools that don't just automate routine tasks but fundamentally change how we approach patient care and drug development feels limitless. Both presentations reinforced that we're now building the infrastructure that could accelerate how quickly life-saving treatments reach patients. The timing feels perfect. We have AI capabilities that can scale impact in ways that weren't possible even two years ago.&lt;/p&gt;
&lt;p&gt;Thanks to &lt;a href="https://www.linkedin.com/in/dricaptain/"&gt;Ilya Captain, PhD&lt;/a&gt; at &lt;a href="https://www.linkedin.com/company/datadrivenpharma/"&gt;DataDrivenPharma&lt;/a&gt; for organizing this excellent event and to &lt;a href="https://www.linkedin.com/in/louise-liu-phd-mba-b195b3343/"&gt;Louise Liu, PhD, MBA&lt;/a&gt; from &lt;a href="https://www.linkedin.com/company/hill-research/"&gt;Hill Research&lt;/a&gt; for the introduction to Tommy and recommending I attend.&lt;/p&gt;
&lt;p&gt;What tools are you building to scale your impact? Curious to hear what others are working on in this space.&lt;/p&gt;
&lt;p&gt;PS: Happy to have been able to chat with Eric and Tommy&lt;/p&gt;
&lt;p&gt;-- Original post link &lt;a href="https://www.linkedin.com/posts/ericmerle_digitalhealth-ai-datascience-activity-7344158374910996480-ViiO/?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAKTdlUBKWeDvuvNDNpOBmAV1OszCr-W__c"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And &lt;a href="https://www.linkedin.com/in/originalpatrick/"&gt;Patrick Hofmann&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;A couple of great talks by &lt;a href="https://www.linkedin.com/in/ericmjl/"&gt;Eric Ma&lt;/a&gt; and &lt;a href="https://www.linkedin.com/in/%F0%9F%8E%AF-ming-tommy-tang-40650014/"&gt;🎯 Ming "Tommy" Tang&lt;/a&gt; at &lt;a href="https://www.linkedin.com/in/dricaptain/"&gt;Ilya Captain, PhD&lt;/a&gt;’s Data Driven Pharma event last night. Eric made a strong case for data scientists building their own tools. I’m no programmer, but I have dabbled in woodworking and it reminded me of all the jigs I’ve built for various projects.&lt;/p&gt;
&lt;p&gt;There are many facets to the ‘buy vs build’ question and here’s one I think often gets overlooked: If an off the shelf solution is available, will it do precisely what you want? Or will you need to conform your project to it? The answer isn’t always clear cut but it’s worth considering when choosing how to allocate your time and resources.&lt;/p&gt;
&lt;p&gt;-- Original post link &lt;a href="https://www.linkedin.com/posts/originalpatrick_a-couple-of-great-talks-by-eric-ma-and-activity-7343995611756511234-uO_5?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAAKTdlUBKWeDvuvNDNpOBmAV1OszCr-W__c"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Afterwards, in our discussion, Patrick had a great point about the parallel between custom tools and woodworking jigs: you can either make your own jigs or buy them, but if you buy them, you now have to conform your woodworking to the jig, and not the other way around. Little compromises compound against the quality of the final deliverable!&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id="but-where-is-deckbot"&gt;But where is deckbot?&lt;/h2&gt;&lt;p&gt;Ok, I bet you're just like me, you hate making slides, and you want to see DeckBot. You can find it linked &lt;a href="slides-maker.py"&gt;here&lt;/a&gt; as a marimo notebook! To run it, you'll need an OpenAI API key mapped to the &lt;code&gt;OPENAI_API_KEY&lt;/code&gt; environment variable. Download the notebook and run this:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="nv"&gt;OPENAI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sk-your-api-key-here&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uvx&lt;span class="w"&gt; &lt;/span&gt;marimo&lt;span class="w"&gt; &lt;/span&gt;edit&lt;span class="w"&gt; &lt;/span&gt;--sandbox&lt;span class="w"&gt; &lt;/span&gt;/your/path/to/slides_maker.py
&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="and-what-were-the-slides-you-actually-presented"&gt;And what were the slides you actually presented?&lt;/h2&gt;&lt;p&gt;I archived them for posterity &lt;a href="index.md"&gt;here&lt;/a&gt;. Enjoy!&lt;/p&gt;
</content></entry><entry><title>Rethinking LLM interfaces, from chatbots to contextual applications</title><link href="https://ericmjl.github.io/blog/2025/6/14/rethinking-llm-interfaces-from-chatbots-to-contextual-applications/" rel="alternate"/><updated>2025-06-14T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:a76ca6c0-c657-334a-ad8e-545e0667a5a8</id><content type="html">&lt;p&gt;Chat interfaces were a great starting point for interacting with large language models, but they're not the endgame. &lt;strong&gt;My thesis is that we should build LLM applications as contextual tools embedded in structured workflows, not as open-ended chat interfaces.&lt;/strong&gt; This insight came from three converging threads that fundamentally changed how I think about building LLM-powered applications.&lt;/p&gt;
&lt;p&gt;The first thread came from a conversation with my colleague &lt;a href="https://www.linkedin.com/in/michelle-faits/"&gt;Michelle Faits&lt;/a&gt;, who articulated that apps powered by generative AI really need to end up looking less like chat interfaces and more like TurboTax -- where there's a well-defined process that needs to happen, and instead of users filling out forms manually, we ask an AI to help with the form-filling process.&lt;/p&gt;
&lt;p&gt;The second thread was a YouTube video titled "&lt;a href="https://youtu.be/mRqBjKFyfLc?si=9sRDPg-hH5iBLiFf"&gt;AI UX Design: ChatGPT interfaces are already obsolete&lt;/a&gt;" by Alan Pike from Vancouver. In it, he talks about shifting from chatbot to context-native interfaces, a change that's both subtle and dramatic. It's subtle because there's little visible change, but dramatic because the way you interact with the interface changes fundamentally. You're no longer stuck with the drudge work of filling out yet another form, but are instead presented with an AI-powered interface capable of understanding what your next action is likely to be and anticipating it just in time.&lt;/p&gt;
&lt;p&gt;The third thread is Clayton Christensen's "jobs to be done" theory. What I've been noticing is that there are too many ChatGPT copycat clones, and those chat clones don't really help me accomplish the job that I'm trying to do. It takes a different type of interface to make that happen.&lt;/p&gt;
&lt;h2 id="these-threads-converge-on-a-simple-truth"&gt;These threads converge on a simple truth&lt;/h2&gt;&lt;p&gt;What connects TurboTax's structured approach, Pike's context-native interfaces, and jobs-to-be-done theory is this: &lt;strong&gt;the most effective LLM applications will embed AI capabilities directly into well-defined workflows rather than forcing users to articulate their needs through chat.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This means moving from "tell the AI what you want" to "let the AI assist you as you work through a process you already understand."&lt;/p&gt;
&lt;h2 id="the-turbotax-moment"&gt;The TurboTax moment&lt;/h2&gt;&lt;p&gt;Michelle's insight about TurboTax really stuck with me. TurboTax works because it represents a well-defined business process with pretty routine steps that we need to walk through, but some of the steps do require judgment calls. Do you fill out this section or not? And what do you fill in? You need to determine that from context, so there's a little bit of agency for LLM bots inside there. But for the most part, it's just form filling.&lt;/p&gt;
&lt;p&gt;This is a powerful analogy for LLM apps, one that gets to the heart of any app build. The question becomes: how do you go about building a user interface that works like this? When we build chat interfaces, we put a lot of onus on the LLM to make smart decisions on behalf of us. But what if chat wasn't the primary way of interacting? What if we had well-defined business workflows supported by custom apps that just require us to fill out forms in a delightful way?&lt;/p&gt;
&lt;h2 id="the-obsolescence-of-chat-interfaces"&gt;The obsolescence of chat interfaces&lt;/h2&gt;&lt;p&gt;Alan Pike's perspective really crystallized something I'd been feeling. In his talk, he showed how we're moving from text-based interfaces that are powerful but confounding to 90% of people, toward context-native interfaces that inject AI capabilities right where you need them.&lt;/p&gt;
&lt;p&gt;Think about it: we've already started seeing hints of tools pushing chat to the side. ChatGPT has Canvas mode now, where if you ask it to co-author a document, it sticks the chat up in the corner and lets you focus on the work you're doing. But this is still just the beginning.&lt;/p&gt;
&lt;p&gt;Pike showed examples of right-click contextual actions, natural language search that understands intent rather than requiring exact phrases, and date pickers where you can just say "next Thursday at 11" instead of clicking through calendar grids. These represent a fundamental shift in how we think about human-computer interaction.&lt;/p&gt;
&lt;p&gt;I thought the talk was quite good, and I'm embedding it below to share.&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/mRqBjKFyfLc?si=9sRDPg-hH5iBLiFf" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;&lt;h2 id="jobs-to-be-done-theory-meets-llm-apps"&gt;Jobs to be done theory meets LLM apps&lt;/h2&gt;&lt;p&gt;Clayton Christensen's jobs-to-be-done framework is perfect for thinking about LLM applications. When I look at most LLM interfaces, we've become hooked on chat -- but they don't necessarily always help me accomplish the specific job I'm trying to do. Generic chat interfaces put the burden on me to figure out how to express my needs and on the LLM to figure out what I actually want. What if we could do better?&lt;/p&gt;
&lt;p&gt;I think what we're going to see is an evolution of LLM-powered apps from being text and chat driven to being deeply embedded within applications, making it possible to flow through business processes in a way that's much smoother and more delightful than what was possible before. It's not really about agentic capabilities, which are nice, but the winners will be the interfaces that inject LLMs in just the right places -- in the boring work!&lt;/p&gt;
&lt;h2 id="building-deckbot-demonstrates-this-approach"&gt;Building DeckBot demonstrates this approach&lt;/h2&gt;&lt;p&gt;Let me show you what this looks like in practice. I built a Markdown slide deck generator called DeckBot, deliberately avoiding chat as the primary interface because it was too freeform and unreliable.&lt;/p&gt;
&lt;p&gt;Instead of starting with a UI, I began with the data model: defining a &lt;code&gt;Slide&lt;/code&gt; as a Pydantic model with title, content, and type. I tested individual slide generation in a Marimo notebook until each component worked reliably. Then I put them together into a &lt;code&gt;SlideDeck&lt;/code&gt; Pydantic model. This allowed me to compose a &lt;code&gt;SlideDeck&lt;/code&gt; from individually-generated &lt;code&gt;Slides&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The next breakthrough came when I realized I could inject LLM capabilities directly into the data objects themselves. Instead of an agent orchestrating external tools, my data models gained natural language-powered methods:&lt;/p&gt;
&lt;div class="hll"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SlideDeck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Slide&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;talk_title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;edit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Edit the slide at a given index using natural language.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="n"&gt;current_slide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;new_slide&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slidemaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slidemaker_edit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_slide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;change&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_slide&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This represents a fundamental shift: instead of putting all intelligence in a central agent, I distributed it into the data models themselves. Each Pydantic model knows how to manipulate itself based on natural language instructions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;DeckBot sits at step 5 of the maturity ladder I'll describe below&lt;/strong&gt;; it provides LLM-augmented interfaces that understand context and assist with specific tasks, but within a structured framework.&lt;/p&gt;
&lt;h2 id="the-future-of-llm-applications"&gt;The future of LLM applications&lt;/h2&gt;&lt;p&gt;I believe we're going to see LLM applications become more like TurboTax and less like open-ended chat interfaces. These will be applications built around well-defined business processes that users can flow through smoothly, with AI providing assistance at just the right moments.&lt;/p&gt;
&lt;p&gt;There's still a place for agents, but we need to recognize that adoption follows a ladder of maturity:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Unstructured work relying on human intuition&lt;/li&gt;
&lt;li&gt;Documented SOPs and manual processes&lt;/li&gt;
&lt;li&gt;Digital UIs guiding humans through structured processes&lt;/li&gt;
&lt;li&gt;Rule-based automation for predictable parts of workflows&lt;/li&gt;
&lt;li&gt;LLM-augmented interfaces providing contextual assistance&lt;/li&gt;
&lt;li&gt;Semi-autonomous LLM components handling defined subtasks&lt;/li&gt;
&lt;li&gt;Full agent orchestration with human oversight&lt;/li&gt;
&lt;li&gt;Truly autonomous agent systems managing entire business processes&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Customer support agents have emerged as one of the first places for LLM agents, and I suspect it's because customer support as a business process has more or less been well-standardized. The fact that we can "agentify" it stems from decades of process refinement. Other business domains need to undergo similar transformation before they're ready for full agent automation.&lt;/p&gt;
&lt;p&gt;At Moderna, we've embraced generative AI heavily, relying on ChatGPT and custom GPTs. But I know this cannot be the only way we interact with LLMs. There are ways to surgically inject LLMs into workflows so users can accomplish what they're trying to do in a structured fashion, but in a delightfully smooth and flowing way.&lt;/p&gt;
&lt;p&gt;The big lesson I learned building DeckBot is understanding where and when to inject LLMs very surgically into custom LLM applications. It's not about replacing human decision-making with AI decision-making; it's about augmenting human workflows with AI capabilities at precisely the right moments.&lt;/p&gt;
&lt;h2 id="key-principles-for-contextual-llm-applications"&gt;Key principles for contextual LLM applications&lt;/h2&gt;&lt;p&gt;Drawing from the TurboTax insight, Pike's context-native approach, and jobs-to-be-done theory, here are the essential principles I've learned:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start with the data model, not the interface.&lt;/strong&gt; Get clear on what you're actually trying to accomplish and model that as structured data first. Design APIs around those data models that work through clean function calls before adding any LLM capabilities.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Inject LLMs surgically into workflows.&lt;/strong&gt; Identify the specific points where natural language understanding or generation adds value, rather than building everything around chat or agents.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test with structured examples first.&lt;/strong&gt; Use notebooks to validate that your core functions work properly before thinking about user interfaces.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build for the job-to-be-done.&lt;/strong&gt; Don't chase the latest agentic capabilities just because they're exciting. Focus on making specific workflows easier and more delightful.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="the-path-forward"&gt;The path forward&lt;/h2&gt;&lt;p&gt;Chat was the beginning of our journey with LLMs, but it is most certainly not the destination. The three threads I described, namely, Michelle's TurboTax-esque structured approach, Pike's context-native interfaces, and Christensen's jobs-to-be-done framework, all point toward the same future: &lt;strong&gt;LLM applications that flow smoothly through business processes, where AI assistance appears exactly when and where it's needed.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This isn't about replacing human decision-making with AI decision-making. It's about augmenting human workflows with AI capabilities at precisely the right moments, without forcing users to translate their intentions into chat prompts or rely on agents to make all decisions for them.&lt;/p&gt;
&lt;p&gt;We're at the beginning of an incredible generation of software and products, and it's an exciting time to build not just the software but the processes around them too! The question we have now is this: how quickly we can move beyond chat alone to build contextual applications that truly help people accomplish their goals?&lt;/p&gt;
</content></entry><entry><title>Principles for using AI autodidactically</title><link href="https://ericmjl.github.io/blog/2025/6/7/principles-for-using-ai-autodidactically/" rel="alternate"/><updated>2025-06-07T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:0983e738-2271-3166-ad98-defee09d63c1</id><content type="html">&lt;h2 id="we-need-to-move-beyond-passive-consumption"&gt;We need to move beyond passive consumption&lt;/h2&gt;&lt;p&gt;Imagine having a personal tutor who's absorbed millions of books, papers, and discussions across every field of human knowledge. That's essentially what Large Language Models (LLMs) offer us today. As David Duvenaud aptly describes them, LLMs are a "&lt;a href="https://x.com/DavidDuvenaud/status/1895139380198584794"&gt;galaxy brain&lt;/a&gt;" of knowledge waiting to be tapped.&lt;/p&gt;
&lt;p&gt;But &lt;em&gt;having access&lt;/em&gt; to information isn't the same as &lt;em&gt;learning from it&lt;/em&gt;. The difference lies in how we engage with these AI tools - passively consuming their outputs versus actively using them to expand our understanding. Through my interviews with researchers and digital professionals, I've discovered patterns in how the most effective learners use AI autodidactically - teaching themselves with AI as their assistant, not their replacement.&lt;/p&gt;
&lt;h2 id="lessons-from-autodidactic-ai-users-at-work"&gt;Lessons from autodidactic AI users at work&lt;/h2&gt;&lt;p&gt;I have conducted many interviews at work about how folks in Moderna's Research and Digital organizations use AI. While the discussions are insanely specific to work and sometimes touch on IP that I cannot reveal, there are principles and patterns in what I observe the best folks do when using AI in their day-to-day work to learn new stuff.&lt;/p&gt;
&lt;h3 id="generate-a-personalized-syllabus-for-learning"&gt;Generate a personalized syllabus for learning&lt;/h3&gt;&lt;p&gt;They recognize that any kind of learning involves effort and hard work, and that the pain of the process is a non-negotiable to make anything stick. So instead of using AI to do stuff for them, they start by using AI to provide a tailored syllabus that allows them to progressively move up the knowledge ladder with increasing effort.&lt;/p&gt;
&lt;p&gt;This is what I would call "scaffolding a personalized syllabus". Their prompts here often include a bit of their current role, their prior training, their own objectives for learning, and what they know from prior experience about how they learn best. On the basis of the syllabus, iterate and follow up.&lt;/p&gt;
&lt;h3 id="apply-one-s-ability-to-think-critically-to-llm-outputs"&gt;Apply one's ability to think critically to LLM outputs&lt;/h3&gt;&lt;p&gt;They recognize that questions are a great way to learn, so they will continuously question and LLM to draw out answers. The act of generating a question as a human is part of the effort needed.&lt;/p&gt;
&lt;p&gt;They apply the skill of critical thinking to the answers generated by an LLM, asking questions such as, "if this is true..." or "is this coherent with...".  They do not blindly accept the output of an LLM!&lt;/p&gt;
&lt;p&gt;Apart from self-coherence with what they know, they verify by cross-checking reputable sources on the internet -- scholarly literature, expert writing, etc.&lt;/p&gt;
&lt;p&gt;At a meta-level, if they find an angle that demands explanation, knowing that sometimes an LLM can be blinded by conversation history, they will explicitly prompt an LLM on contrary points, using prompts that start with, "but I remember that..." or "this sounds suspicious, could it be that..."&lt;/p&gt;
&lt;p&gt;Also, in the absence of another human, they use LLMs to provide initial critique about what they have produced (e.g. in writing form). They use LLMs in the same way jazz musicians riff off one another.&lt;/p&gt;
&lt;h2 id="what-s-the-core-trick"&gt;What's the core trick?&lt;/h2&gt;&lt;p&gt;At its core, the main "trick" to using an LLM autodidactically is to avoid delegating critical thinking to the LLM and instead applying the full force of one's agency. We need to leverage the galaxy brain of knowledge from its training set (and, where applicable, internet search capabilities) and apply individual effort by critically thinking through LLM outputs. Essentially, every skill we were taught to hone in literature class in high school, debate club in junior college, science philosophy class in undergrad, and scientific journal clubs during graduate training!&lt;/p&gt;
&lt;p&gt;AI has brought the philosophical points of human agency into sharp relief. Like any tool, LLMs can be used to increase your agency or diminish it. It's a double-edged sword. Use it for the former!&lt;/p&gt;
</content></entry><entry><title>The invisible polish of automatic model routing</title><link href="https://ericmjl.github.io/blog/2025/5/25/the-invisible-polish-of-automatic-model-routing/" rel="alternate"/><updated>2025-05-25T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:12869f09-d6d3-311d-92c1-16fdf5ec643c</id><content type="html">&lt;p&gt;I've been using Cursor's latest updates, and while the surface-level improvements are nice—better edge rounding, refined colors, thoughtful layering—there's one change that's got me genuinely excited: automatic model routing.&lt;/p&gt;
&lt;p&gt;No more model picker. No more stopping mid-thought to decide between OpenAI's models, Claude, or whatever other model might be appropriate for my current task. Cursor just figures it out and routes my request to the right model automatically.&lt;/p&gt;
&lt;h2 id="why-model-pickers-are-ui-bugs"&gt;Why model pickers are UI bugs&lt;/h2&gt;&lt;p&gt;I remember reading somewhere (probably on Twitter, let's be honest) that model pickers are fundamentally a UI bug. The argument was simple: users shouldn't need to understand the technical differences between models to get their work done. They should just describe what they want, and the system should handle the rest.&lt;/p&gt;
&lt;p&gt;At the time, I nodded along but didn't fully appreciate how right this was until I experienced Cursor's implementation. Before this change, I was making micro-decisions about model selection multiple times per day. Should I use the faster model for this simple refactoring? Do I need the more capable model for this complex architectural question? Each decision was small, maybe taking 2-3 seconds, but they added up.&lt;/p&gt;
&lt;h2 id="the-cognitive-tax-of-micro-decisions"&gt;The cognitive tax of micro-decisions&lt;/h2&gt;&lt;p&gt;These tiny decisions represent what I think of as cognitive tax: small mental overhead that accumulates throughout the day. Each model selection forced a brief context switch: I had to step out of my coding flow, evaluate the complexity of my request, weigh speed versus capability, and make a choice.&lt;/p&gt;
&lt;p&gt;The individual cost was negligible. The cumulative cost was not. By the end of the day, I'd made dozens of these micro-decisions, each one pulling a small amount of mental energy away from the actual problem I was trying to solve.&lt;/p&gt;
&lt;p&gt;Cursor's automatic routing eliminates this entirely. I describe what I want, hit enter, and trust that the right model will handle it. The decision-making burden shifts from me to the system, where it belongs.&lt;/p&gt;
&lt;h2 id="parallels-to-apple-s-design"&gt;Parallels to Apple's design&lt;/h2&gt;&lt;p&gt;This reminds me of something I read about Apple's design philosophy during the Jony Ive era. The idea more than making things look beautiful, it was about removing friction at every possible level, even in places users might not consciously notice.&lt;/p&gt;
&lt;p&gt;Think about the original iPhone's lack of a keyboard. Everyone said it was crazy, that people needed physical keys. But Apple understood that the mental model of "keyboard for typing" was actually limiting. By removing the physical keyboard, they freed up space for context-sensitive interfaces that could adapt to what you were actually trying to do.&lt;/p&gt;
&lt;p&gt;Cursor's automatic model routing feels like the same kind of thinking. Instead of optimizing the model picker interface, they eliminated the need for it entirely. The best interface is often no interface at all.&lt;/p&gt;
&lt;h2 id="the-broader-principle"&gt;The broader principle&lt;/h2&gt;&lt;p&gt;What makes this interesting isn't just that it saves me a few seconds per day. It's that it represents a shift in how we think about AI tool design. Instead of exposing the complexity of the underlying system to users, we can build intelligence into the routing layer itself.&lt;/p&gt;
&lt;p&gt;This has implications beyond just model selection. How many other micro-decisions are we forcing users to make that could be automated away? How many interface elements exist because we haven't figured out how to make them unnecessary?&lt;/p&gt;
&lt;p&gt;I suspect we'll see more of this pattern as AI tools mature. The first generation of AI interfaces were necessarily explicit: users needed to understand models, parameters, and context windows because the tools couldn't make those decisions reliably. But as the underlying systems get smarter, the interfaces can get simpler.&lt;/p&gt;
&lt;h2 id="the-invisible-improvements"&gt;The invisible improvements&lt;/h2&gt;&lt;p&gt;The best improvements are often the ones you don't notice consciously but feel in your workflow. Cursor's automatic model routing is exactly this kind of enhancement. I don't think about it while I'm coding, but I feel its absence when I use other tools that still require manual model selection.&lt;/p&gt;
&lt;p&gt;This is the kind of polish that compounds. Each eliminated micro-decision, each removed point of friction, each automated choice creates space for deeper focus on the work that actually matters. It's not revolutionary on its own, but it's part of building tools that feel like extensions of thought rather than obstacles to it.&lt;/p&gt;
&lt;p&gt;The question for other AI tool builders is: what other invisible friction exists in your interfaces? What decisions are you forcing users to make that your system could handle automatically? The model picker was just the beginning.&lt;/p&gt;
</content></entry><entry><title>Supercharge your coding agents with VSCode workspaces</title><link href="https://ericmjl.github.io/blog/2025/5/24/supercharge-your-coding-agents-with-vscode-workspaces/" rel="alternate"/><updated>2025-05-24T00:00:00Z</updated><author><name>Eric J. Ma</name></author><id>urn:uuid:d299229c-1aa0-384c-a2e2-cb36c3d97384</id><content type="html">&lt;p&gt;I was building out my &lt;a href="https://github.com/ericmjl/building-with-llms-made-simple"&gt;LLM tutorial repository&lt;/a&gt; for &lt;a href="https://www.scipy2025.scipy.org/"&gt;SciPy 2025&lt;/a&gt; and found myself constantly switching between windows—improving the &lt;a href="https://github.com/ericmjl/llamabot"&gt;LlamaBot&lt;/a&gt; library in one window, then flipping to my tutorial repo in another to update examples that used the new features. Every time I added a new method or changed an API in LlamaBot, I had to remember to update the corresponding tutorial examples. The constant context switching was slowing me down and making it easy to miss places where the tutorial needed updates.&lt;/p&gt;
&lt;p&gt;Then I discovered something that changed how I code across repos: Workspaces! They aren't just convenient for organizing multiple repositories, they're also game-changers for coding agents in Cursor.&lt;/p&gt;
&lt;p&gt;When you add multiple repositories to the same workspace, your coding agent magically gains context across all your repos simultaneously. No more window switching, no more explaining relationships between codebases. Instead, your coding assistants can access code in multiple repositories at once.&lt;/p&gt;
&lt;p&gt;Here's how to set this up and why it matters.&lt;/p&gt;
&lt;h2 id="setting-up-your-first-multi-repo-workspace"&gt;Setting up your first multi-repo workspace&lt;/h2&gt;&lt;h3 id="step-1-create-the-workspace"&gt;Step 1: Create the workspace&lt;/h3&gt;&lt;p&gt;Open a blank Cursor/VSCode window and immediately save it as a workspace file (File → Save Workspace As). I recommend saving it outside any repository; I keep mine as a sibling directory to my repos, like &lt;code&gt;~/github/llm-scipy-tutorial.code-workspace&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Then, go to File → Add Folder to Workspace to add your first repository (my main tutorial project &lt;code&gt;~/github/building-with-llms-made-simple&lt;/code&gt;), and repeat for your second repo (the companion library I was improving, &lt;code&gt;~/github/llamabot&lt;/code&gt;). You'll see both folders appear in the Explorer sidebar.&lt;/p&gt;
&lt;h3 id="step-2-watch-the-magic-happen"&gt;Step 2: Watch the magic happen&lt;/h3&gt;&lt;p&gt;Here's where it gets interesting. Fire up Cursor's AI or GitHub Copilot and give it a specific prompt that references files across both repos. Try something like:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;"Look at @llamabot/llamabot/bot/simplebot.py and edit @building-with-llms-made-simple/notebooks/03_advanced_bot.py to update the StructuredBot example for document summarization."&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(If you're using VSCode instead of Cursor, just replace the @ symbols with #.)&lt;/p&gt;
&lt;p&gt;Your agent can now see both codebases simultaneously. It understands how your LlamaBot library works and can create coherent examples in your tutorial repo, suggesting coordinated changes across repos while maintaining consistency between your library code and tutorial examples.&lt;/p&gt;
&lt;h3 id="step-3-reopening-your-workspace"&gt;Step 3: Reopening your workspace&lt;/h3&gt;&lt;p&gt;Next time you open Cursor or VSCode, you'll see your workspace listed on the welcome screen under "Recent". Click it to instantly load all your repositories with the same folder structure and settings.&lt;/p&gt;
&lt;h2 id="quick-tips-that-make-this-even-better"&gt;Quick tips that make this even better&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Keep workspaces outside repositories:&lt;/strong&gt;
I always save workspace files as siblings to my repo directories, never inside them. This prevents workspace files from accidentally getting committed and keeps things clean when you're working across multiple projects.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;~/github/
├── llamabot/
├── building-with-llms-made-simple/
└── llm-scipy-tutorial.code-workspace
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Quick tip on scale:&lt;/strong&gt;
You can add as many repositories as you need. I've had workspaces with multiple model experiment repos, shared data utilities, and production pipelines, and Cursor's agent could reference files across all of them. When you use &lt;code&gt;@workspace&lt;/code&gt; in Cursor, it considers every file in every repository. Fair warning though—I recently worked across 5 repos at work and my head was spinning even with LLM help. Sometimes less is more.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Be prescriptive with file references:&lt;/strong&gt;
Prompting across repos works best when you can pinpoint exactly which file to reference or edit. In Cursor, use &lt;code&gt;@&amp;lt;file&amp;gt;&lt;/code&gt; syntax, while in VSCode it's &lt;code&gt;#&amp;lt;file&amp;gt;&lt;/code&gt;. This helps the agent focus on the specific files you care about rather than wandering through your entire workspace.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use this pattern strategically:&lt;/strong&gt;
This approach shines when your current project depends on functionality that was developed beforehand in other repositories. Think data science projects that depend on internal tools built by other teams, or tutorial repositories that need to stay consistent with the underlying library they're demonstrating. When you have models or analyses that depend on utilities, libraries, or frameworks developed separately, workspaces let your coding agent understand both the dependency and the dependent code simultaneously. For single-repo exploratory work, stick to regular folders.&lt;/p&gt;
&lt;p&gt;That's it. Next time you're coordinating changes across multiple data science repositories, set up a workspace and let your coding agent see the full picture.&lt;/p&gt;
</content></entry></feed>