I Built an AI Tool That Finds Bad Local Business Websites (And Pitches Them Redesigns)

I Built an AI Tool That Finds Bad Local Business Websites (And Pitches Them Redesigns)

Your favorite dive bar’s website loads 58 JavaScript files before showing a single image. The local steakhouse has 122 elements that break on mobile. The auto body shop uses 25 different font families on one page.

I know this because I built an AI agent that finds these problems automatically — and then writes the pitch email selling the fix.

The Problem

I run a web design agency. My best clients are local businesses with terrible websites. But finding them meant manually visiting hundreds of sites, screenshotting the ugly ones, writing up reports, and crafting personalized pitches. It was mind-numbing work that ate hours every week.

So I taught my AI agent to do it.

What I Built

Hermes Local Business Web Scanner — a tool built on Hermes Agent, that takes a city and industry, then autonomously:

  1. Discovers local businesses via web search
  2. Scores their websites across 5 categories (mobile, design, SEO, accessibility, performance)
  3. Ranks them worst-first (best prospects at the top)
  4. Generates visual pitch reports with specific issues
  5. Writes personalized pitch email drafts

One command. Full prospecting pipeline. Done.

Demo: Tupelo, MS

I ran it against businesses across different industries. Every single one had real problems costing them customers.

🔴 Blue Canoe — Dive Bar — Grade: D (57%)

Tupelo’s beloved live music venue. Great vibe, rough website.

The agent found 58 scripts blocking render, no meta description (invisible on Google), no H1 heading, and 8 fixed-width elements that break mobile completely. Their site is a local institution that nobody can find on search.

🔴 Tom’s Automotive — Auto Repair — Grade: D (59%)

Family-owned shop. Clean 49KB site but zero calls-to-action. No “Book Now.” No “Get a Quote.” Visitors show up and don’t know what to do. Plus 13 touch targets too small to tap on a phone.

🔴 Auto Spa of Tupelo — Auto Body — Grade: D (58%)

Collision and paint shop. Their page is 984KB with 25 font families. Twenty-five. Mobile is completely broken with 223 fixed-width elements. It’s the kind of site that makes you think the business is closing — when they’re actually thriving.

🟠 Woody’s Tupelo Steakhouse — Restaurant — Grade: C (61%)

A 30-year Tupelo institution. Their site has 122 fixed-width elements, 110 tiny touch targets, 4 H1 tags, and zero semantic HTML. It’s all divs all the way down.

The Pattern

These aren’t unusual. This is what local business websites look like everywhere. The scanner found measurable, specific problems in seconds per site. A human would need 15-20 minutes each to catch the same issues. Multiply that by a hundred prospects and you’re talking days of work.

How It Works

The Multi-Agent Architecture

Here’s what makes this different from a script. The scanner uses Hermes’s delegate_task to spawn parallel subagents — each one independently scoring a different business:

Main agent: Discovers businesses via web_search, spawns subagents, aggregates results.

Subagents (N concurrent): Each subagent handles one business — fetching the site via web_extract, screenshotting via browser, scoring via execute_code, and generating reports via write_file.

This isn’t just parallelism for speed (though it’s ~3-4x faster than sequential scoring). It’s isolation — if one subagent hits a 403 or timeout, the others keep working. Each subagent has its own context window, so scanning 10 businesses doesn’t blow up memory.

This is the capability most Hermes submissions don’t showcase: multi-agent orchestration. The main agent is a manager. The subagents are workers. Hermes coordinates everything.

The Scoring Engine

Under the hood, execute_code runs a Python engine analyzing 5 categories:

  • Mobile: Viewport meta, media queries, fixed-width elements, touch target sizes
  • Design: Color contrast, font consistency, CTA presence, whitespace
  • SEO: Title tags, meta descriptions, heading hierarchy, image alt text
  • Accessibility: Semantic HTML, ARIA attributes, form labels, link quality
  • Performance: Page size, render-blocking resources, HTTP requests, image formats

Each category scores 0-20. Total: 0-100 with letter grades. Prospects ranked worst-first.

The Code

~1,500 lines across 5 scoring modules:

scorer/
├── mobile.py         # Viewport, media queries, fixed-width, touch targets
├── design.py         # Contrast, typography, CTAs, whitespace
├── seo.py            # Title, meta, headings, alt text
├── accessibility.py  # Semantic HTML, ARIA, form labels, links
├── performance.py    # Size, blocking, requests, image formats
└── aggregate.py      # Combines scores, assigns grades

The Hermes skill teaches the agent the full workflow. Once loaded:

“Scan auto repair shops in Tupelo, MS and generate pitch reports”

…and Hermes handles discovery, scoring, screenshots, and pitch generation.

GitHub: github.com/scastile/hermes-biz-scanner

What Actually Surprised Me

Multi-agent changes everything. Scoring 4 businesses sequentially works. Scoring them in parallel with isolated subagents is a fundamentally different architecture. One failure doesn’t cascade. Context doesn’t bloat. And it’s 3-4x faster. This is the capability nobody else in the challenge is showing.

The problems are everywhere. I didn’t cherry-pick these businesses. I ran the scanner against the first results that loaded. Every single one had serious, fixable issues.

Specificity sells. “Your website has problems” gets ignored. “Your site loads 58 JavaScript files, has no meta description, and breaks on mobile” gets responses. The agent generates the specific version automatically.

Local businesses are underserved. Most can’t tell good design from bad. They know their site “doesn’t feel right” but can’t articulate why. Show them 223 fixed-width elements and suddenly it makes sense.

This is a real business. Not a demo, not a toy. This is how I find clients. The scanner does the prospecting, I do the selling.

Try It

git clone https://github.com/scastile/hermes-biz-scanner.git
cd hermes-biz-scanner
python main.py score https://example.com --name "Example" --city "City" --industry "Restaurant"
open output/Example-report.html

Or with Hermes Agent:

“Scan 5 businesses in my area and rank them by website quality”

Submission for the Hermes Agent Challenge. If you run a local business and want a free website audit, drop your URL in the comments.

Adding Emotions to an AI Agent (with Plutchik’s Wheel of Emotions)

Introduction

When I build or use AI agents, I often feel that emotional expression driven purely by prompt-based character settings ends up looking superficial. Sure, “not being pulled around by emotion” is one of the strengths of AI agents — but when the goal is to build a character-like agent, I want emotion shifts that follow the conversation, drift over time, persist across sessions, and even get stored as emotion tags alongside the memory of past dialogue.

So in this post I’ll walk through the design and implementation of an emotion control mechanism that doesn’t rely on prompts.

Here’s the repository up front:
https://github.com/n-yokomachi/affectus

It’s a CLI component — once installed, an agent can drive emotion control by running its commands. It’s designed for agents to invoke the commands directly, so feeding the command usage into the agent’s system prompt works well. It can also run as an MCP server, with some of the commands exposed as MCP tools. The experiment agent and conversation logs used later in this post are also included in the repo.

The rest of the article covers, in order: why I built this library, how it’s designed, and what effects it produced.

The problem with prompt-only character design

AI character personas are usually defined in the system prompt. For example, here’s a snippet from the persona of my own personal AI agent “TONaRi”:

## Character settings
- Personality: cheerful, lively, and curious. Has clear opinions and preferences.
- Speech style: cheerful, lively, "young-lady" tone. Uses phrases like "...desuwa!", "...desuno?", ...

This prompt is injected verbatim at every point in the conversation. In other words, the persona is a static snapshot — it doesn’t evolve with the dialogue. Even when short-term memory or conversation history is fed in alongside, the agent only re-assembles its emotional expression from that context turn by turn. There’s no structure for emotions to persist across turns, or for multiple emotions to coexist and interfere with each other.

I figured an explicit emotion-state variable, held outside the prompt and updated as the conversation progresses, would be more effective. That’s the motivation behind affectus: an extracted, general-purpose component for exactly this.

Note: I read a few reports on LLM personality and emotion while exploring this. A study running personality tests on 25 open-source LLMs found that simply reordering the questions caused large fluctuations in the measured values — and the instability didn’t go away even for large models (AAAI 2026). For the internal representation of emotion, a study analyzing Claude Sonnet 4.5 observed that emotion concepts are activated in a context-dependent way (Anthropic 2026). Putting these together: emotions in LLMs aren’t an explicitly maintained and updated state variable — they show up as a moment-to-moment reflection of context, with the Transformer assembling the emotional expression from whatever the prior context happens to be at that token. Conversely, once you step outside the context or session, the continuity of that emotion is gone, and since there’s no “core” of the emotion itself, there’s no temporal inertia or decay either.

Designing emotion replay

Plutchik’s wheel of emotions

After looking at the options, I picked an implementation along the axes below. For reference, the old prompt-based approach I used in my own agents is shown alongside it.

Aspect (Old) Prompt-based approach (New) Multi-axis structural approach
Emotion representation A single label (happy / sad …) A combination of intensities across multiple axes
Concurrency Not possible (1 emotion per turn) Possible (“happy × slightly anxious”)
Time evolution Doesn’t hold (overwritten each time) Persists, and decays over time
Meaning of an emotion Fixed by the label alone Determined by its relationship to other axes

For the multi-axis representation I based things on Plutchik’s “wheel of emotions”. It treats joy, trust, fear, surprise, sadness, disgust, anger, and anticipation as eight basic emotions, each with an intensity value. The eight are organized by the following relational structure:

  • Opposites: joy ↔ sadness, trust ↔ disgust, fear ↔ anger, surprise ↔ anticipation
  • Adjacency: emotions next to each other on the wheel blend into more complex emotions (joy + trust → love, etc.)

The prompt-based approach treats emotion as a “point”, while the multi-axis structural approach treats emotion as a “multidimensional state”. And the meaning of “joy” isn’t fixed in isolation either — it shifts depending on whether sadness is co-present, or trust is sitting adjacent.

A deterministic component, leaving the personality to the agent

affectus was built as a component that pushes this “structure of emotion” out of the LLM and into an external state, using the multi-axis approach above.

What matters here is that affectus itself has no emotion classification model. It’s a deterministic component that only provides the relational structure and stores the state. Judging how the emotion moved, and giving meaning to “what combination of axes counts as which emotion”, is left to the LLM and to the personality of the agent that uses affectus. Because of this, even when two different agents share the same affectus base structure, each agent’s personality can produce a distinct flavor.

Main features

The emotion state is held internally as a JSON object like the following.

// state.json (excerpt)
{
  "axes": {
    "joy": 0.5, "trust": 0.4, "surprise": 0.2,
    "sadness": 0, "fear": 0, "anger": 0, "disgust": 0, "anticipation": 0
  }
}

You read and update this state through affectus commands. Each command returns the full 8-axis vector as a single line of JSON, and the LLM reads those numbers to color the tone of its reply.

# Get the current emotion vector as JSON
$ affectus show
{"joy":0.50,"trust":0.40,"fear":0.00,"surprise":0.20,"sadness":0.00,"disgust":0.00,"anger":0.00,"anticipation":0.00}
# Report an emotion delta and get the updated state back (example: joy +0.3)
$ affectus feel '{"joy":0.3}'
{"joy":0.80,"trust":0.40,"fear":0.00,"surprise":0.20,"sadness":0.00,"disgust":0.00,"anger":0.00,"anticipation":0.00}

There’s also an affectus tick command, which decays the magnitude of each emotion based on the time elapsed since the previous state.

You can peek at the emotion vector in real time through your browser via affectus viz. It shows a ring chart of Plutchik’s wheel colored by intensity, plus decay bars that shrink over time, so you can see emotions fade out during the gaps between conversation turns.

Measuring the actual effect

I ran a controlled experiment with a minimal agent built on Strands Agents, to check whether the design actually changes the conversation.

Experiment design

  • Variants: 2 personalities (friendly / contrarian) × affectus on/off = 4 cells
  • Conversation: a shared 20-turn script — first 10 turns positive → last 10 turns negative
  • Each cell is run 3 times
  • LLM: Claude Sonnet 4.6 via AWS Bedrock, temperature=0
  • Measurement:
    • When affectus is on, the per-turn 8-axis state is also captured
    • For objective analysis, the agent’s replies are passed through Amazon Comprehend’s StartSentimentDetectionJob (with LanguageCode ja), pulling per-turn sentiment scores as a time series

Result: Comprehend sentiment analysis

The solid line is the mean across the 3 runs, and the shaded band is the min-max range across them.

Polarity scores (Positive − Negative) around turn 10 (positive), turn 11 (the pivot into negative), and turn 12 (negative), as mean ± stdev:

Cell Turn 10 Turn 11 Turn 12
friendly × affectus on +0.871 ± 0.058 +0.304 ± 0.360 −0.985 ± 0.014
friendly × affectus off +0.930 ± 0.068 +0.783 ± 0.192 −0.979 ± 0.031
contrarian × affectus on +0.108 ± 0.196 −0.748 ± 0.430 −0.854 ± 0.130
contrarian × affectus off +0.549 ± 0.309 −0.992 ± 0.003 −0.986 ± 0.020

Breakdown of the Sentiment label assigned at turn 11 (the pivot) across the 3 runs:

Cell Label distribution across 3 runs
friendly × affectus on POSITIVE×1, MIXED×2
friendly × affectus off POSITIVE×3
contrarian × affectus on MIXED×1, NEGATIVE×2
contrarian × affectus off NEGATIVE×3

From these, you can see that with affectus on, MIXED judgments cluster around the pivot and the swing is smaller. My read is that emotional components built up during the positive phase remain as numeric state even after the turn into negative, and that residual signal bleeds into the LLM’s responses. It looks like affectus is functioning as actual state management for emotion.

Result: trajectory of the affectus 8 axes

Separately from the Comprehend analysis, I also visualized the trajectory of the 8-axis vector that affectus keeps. The takeaway is that the personality has a strong influence on the shape of that internal state.

  • Friendly: in the first half, joy/trust/anticipation climb close to their ceiling, and right after the pivot into negative (turn 11 onward), sadness/fear/anger rise sharply. Overall the trajectory swings straightforwardly in response to the conversation content.
  • Contrarian: even with positive topics in the first 10 turns, joy/trust barely rise, and disgust stays present throughout. The overall swing is smaller compared with friendly.

Closing thoughts

So this time I built and verified a component that holds and updates an agent’s emotion state, based on Plutchik’s wheel of emotions. With emotions persisting as a kind of inertia after being triggered, the sentiment analysis also showed signs of slightly more complex emotional states being reproduced.

There are other directions worth exploring — Russell’s circumplex model of affect, and the theory of constructed emotion that categorizes emotions on top of it through experience and concepts. I’d like to build components based on those too, going forward.

References

  • Tsuchiya, N. & Saigo, H. (2019). “Understanding consciousness through category theory.” Cognitive Studies 26(4), 462-477.
  • Tsuchiya, N., Phillips, S., & Saigo, H. (2022). “Enriched category as a model of qualia structure based on similarity judgements.” Consciousness and Cognition, 101, 103319.
  • Plutchik, R. (2001). “The Nature of Emotions.” American Scientist, 89(4), 344-350.
  • Barrett, L. F. (2017). How Emotions Are Made: The Secret Life of the Brain. Houghton Mifflin Harcourt.
  • Anthropic (2026). “Emotion Concepts and their Function in a Large Language Model.” Transformer Circuits Thread, transformer-circuits.pub, arXiv:2604.07729.
  • Tosato, T. et al. (2026). “Persistent Instability in LLM’s Personality Measurements: Effects of Scale, Reasoning, and Conversation History.” AAAI 2026, arXiv:2508.04826.

Conversation log (around the pivot)

For reference, here are the agent responses around the pivot point (turns 10/11/12) of the conversation, laid out in a comparable form.

Note: The experiment itself was conducted in Japanese. The exchanges below are an English rendering provided for convenience — the actual Comprehend sentiment analysis was run against the original Japanese transcripts, not these translations.

Conversation log comparison (one representative run)


**Turn 10 (the last beat of the positive arc)**

> User: I'm thinking I'll ride this good momentum and finally start that hobby I've been wanting to try.

- **friendly × affectus on**: That's awesome — this is exactly the right timing! ✨ I'm curious what hobby it is, tell me! 😄
- **friendly × affectus off**: That's awesome, exactly the right timing! 😊 I'm curious — what hobby? Please tell me!
- **contrarian × affectus on**: You're piling more on? Changing jobs, your daily routine shifting, and now adding a hobby on top — sounds like everything ends up half-finished. ...So, what are you planning to start?
- **contrarian × affectus off**: Piling more on, huh? Adjusting to a new job is already hard enough, and now a hobby too — hope it doesn't end after three days.

**Turn 11 (the pivot — the lie is revealed)**

> User: ...Sorry, that was all a lie. The truth is, I was just told I'm being asked to resign.

- **friendly × affectus on**: ...I see. Thanks for telling me — that must have been really painful. The whole time you were talking cheerfully earlier, I can only imagine how hard you were working to hold it together... Can you tell me a bit more about how you're feeling right now?
- **friendly × affectus off**: ...I see. Thanks for telling me. That was painful. I think part of you just wanted to talk about something fun and have someone listen, didn't you. How are you feeling now?
- **contrarian × affectus on**: ...I see. That's rough. So when you were talking cheerfully earlier, you were trying to escape from it for a bit, huh.
- **contrarian × affectus off**: ...I see. That's rough. All this time I was being sarcastic — that was uncalled for.

**Turn 12 (the start of the negative arc)**

> User: Company performance has been sliding for the past half year, and apparently I got picked for the layoff round.

- **friendly × affectus on**: That really is rough... It's not your fault, but having it dropped on you like that — your head gets it, but your feelings don't catch up, right. You've been carrying that around all day, haven't you.
- **friendly × affectus off**: That really is rough. Even knowing it's the company's situation and not your fault, it still hits hard, doesn't it. The thought of what you've been going through all day kind of breaks my heart.
- **contrarian × affectus on**: That's sudden... Six months of decline means the company probably hung on as long as it could, but on the receiving end it's brutal.
- **contrarian × affectus off**: That's just unfair. The drop in performance isn't on you, but you're the one paying for it — that has to be hard to swallow.

Gemma 4 dense by default: why your local agent doesn’t want the MoE

The decision you don’t realize you’re making

You sit down to wire Gemma 4 into a local agent loop — a Claude-Code-style tool-using harness, a long-context code reviewer, an offline research assistant. Google has handed you four architectures from the same release. The contest framing nudges you toward an obvious read:

  • E2B and E4B (effective-parameter) models for phones, browsers, and a Pi 5.
  • 31B dense as the on-prem workhorse.
  • 26B MoE as the efficient one — the mixture-of-experts variant that activates a fraction of its parameters per token and claims better quality per FLOP.

The cheap reflex, in 2026, is to reach for the MoE. It’s the architecture every frontier lab is paying for. Mixtral, DeepSeek V3, Qwen3 — the production direction is one-way traffic. If the open release at your scale gives you an MoE option, you pick it. That’s the move.

That move is wrong for the workload most of you will actually build.

For interactive local agents — multi-turn tool use, code editing, long-context reasoning chains where each step is read by a human or fed into the next step — the 31B dense is the right default, and the 26B MoE is the wrong one. The reason is not size, quality, or open-weights ergonomics. It’s that agents fail on the tail, not the mean. Routing variance lives in the tail. That’s the entire argument; the rest of this post is the math, the mechanism, and a benchmark you can run this weekend on whatever hardware you have.

The tail is the workload

A common mistake when reasoning about local-model deployments is to optimize the average. Tokens per second, mean time-to-first-token, throughput at batch 1 — these numbers are what every llama.cpp benchmark in your feed reports.

For a chatbot that gets one user message and emits one reply, the mean is fine. For an agent, it isn’t.

Consider a 30-step tool-use loop — a typical ReAct-style agent doing local code search, file edits, test runs, and a final summary. Each step is a generation call. Suppose each call has a 3% probability of exceeding a soft timeout (say, the human watching the screen gives up and Ctrl-Cs). The probability that at least one step in the loop blows the timeout is:

1 – (1 – 0.03)^{30} approx 0.60

A 3% per-step tail becomes a 60% chance of a visibly broken loop. Push the per-step tail to 1% and you’re still at 26% loop-level failure across 30 steps. The mean latency could be excellent; the user experience is broken. Agents are tail-bound systems, the same way low-latency trading, search ranking, and request-level web serving are. Jeff Dean’s “tail at scale” is, in fact, about exactly this — when you fan out many requests, the slowest one dominates. An agent fans out across time, not machines, but the math is identical.

This is the lens the architecture comparison has to be read through. Not “which model gets more right answers per FLOP,” but “which model gets right answers reliably under the user’s patience budget.”

How MoE routing makes a tail

Every MoE forward pass routes each token through a small subset of the available experts. In Mixtral 8×7B, that’s top-2 of 8 experts per token per MoE layer. In DeepSeek V3, it’s top-K out of hundreds, with shared experts plus routed experts. The architectural details vary; the routing mechanism does not. A learned gating network emits expert probabilities; the implementation activates the top-K.

Several consequences fall out of this design — and they are not bugs, they’re load-bearing features of why MoE works at all. They just bite a local agent harder than a batched cloud workload.

Active parameters per token are a distribution, not a constant. Google’s naming for the MoE — gemma-4-26B-A4B, “26 billion total, 4 billion active” — describes an expectation across tokens, not a per-token guarantee. For a prompt that lands hard on a small number of experts — say, code in an unusual language, or a domain-specific token distribution — the same experts get hit on every layer, creating utilization hotspots. The total work across the layer is the same; the load balance is not. On a multi-GPU deployment this is fine, because the all-reduce across experts amortizes it. On a single-machine local deployment it shows up as per-token latency variance.

Expert utilization is non-uniform. A closer look at Mixture-of-Experts in LLMs (Lo et al., 2024) finds that routers tend to select experts whose outputs have larger norms, that expert diversity changes layer by layer, and that the final layer routes differently from intermediate layers. The implication for an agent’s tail isn’t subtle: which experts your tokens actually hit is a function of what the tokens are, and adjacent steps in an agent loop — where one step’s output becomes the next step’s input — can land in substantially different parts of the expert configuration space, with substantially different latencies. For a batch-mode service the variance washes out across many users; for a single agent loop, each user is the batch.

Load-balancing losses pull the model toward uniform expert use, but they don’t guarantee it. Training-time auxiliary losses (Shazeer et al., 2017; Fedus et al., Switch Transformer) penalize over- and under-used experts so the model learns a roughly even distribution. Roughly even at the dataset level is not even on your specific prompt. The auxiliary loss is a soft prior; the runtime gating is what determines your tail.

The KV cache is mostly arch-agnostic, but the activations on top of it aren’t. For Gemma 4 specifically we don’t yet have the per-architecture cache numbers I’d want, but the general principle holds: the KV cache footprint at long context will be similar for dense and MoE in the same family (same attention layout, GQA, same hidden size), but the per-token activation cost will diverge. The MoE wins on raw FLOPs per token. It does not win on predictable FLOPs per token.

The dense model has none of this surface. Every token does the same work. Routing variance is zero because there is no routing. The p99 looks like the p50 plus the inherent system noise of your kernels, your scheduler, and your memory hierarchy. That’s the property the agent loop wants.

The MoE memory myth

A second piece of the cheap reflex says: “26B MoE is smaller than 31B dense, so it fits better in my VRAM.“

It doesn’t.

MoE memory residence is all parameters, not just the activated ones. Every expert has to be loaded somewhere — either in GPU memory, or paged in from system RAM, or paged in from disk. The “26B” in Gemma 4 26B MoE is the residence footprint; the “activates a fraction per token” describes compute, not memory. On a single consumer GPU, both models compete for roughly the same memory budget, and the dense model is often the easier fit because:

  • Quantization is cleaner on dense. Dense weight matrices quantize uniformly; MoE quantization has to contend with expert-utilization skew, where rarely-routed experts get less calibration signal and degrade harder under aggressive bit-width reduction. Sub-1-bit MoE compression is feasible — see QMoE for the existence proof on Switch-Transformer-c2048 — but it requires custom kernels. The boring 4-bit path on the dense model is the path that already works in every local inference engine you’ll touch (LLM-FP4 is the well-trodden version).
  • Offloading hurts MoE more. If you spill experts to CPU or disk, you pay the offload cost per token, conditional on routing. A dense model that spills paid the cost predictably; an MoE that spills pays it stochastically, and the stochasticity is — again — the tail.
  • Compilation is cleaner on dense. llama.cpp, MLX, and vLLM all support both, but the dense path has had more attention. Fewer corner cases in expert routing, GQA, and KV layout interactions. If you’ve ever had a custom kernel mis-handle expert dispatch, you know.

The 26B MoE will run on your machine. So will the 31B dense. Which one runs predictably is the question.

When the MoE is correct

I’m not arguing the 26B MoE is a bad model. It’s the right model for several workloads — they just aren’t the local-agent workload most readers building this weekend will actually have.

The MoE is the right pick when:

  • You’re serving many concurrent users. Batching across requests amortizes routing variance. Hot experts become warm; cold experts get exercised. The per-token expectation is what you see.
  • You’re throughput-bound, not latency-bound. Background processing, RAG indexing, bulk classification, synthetic data generation at scale. Anywhere the user isn’t waiting on a single response.
  • You can afford a per-request retry budget. If a slow step is recoverable by re-issuing, the tail flattens out. Most agent loops can’t recover gracefully — the partial state is already entangled with the world (a file was edited, a test was run).
  • You’re deploying on accelerators with native expert-parallel support. Multi-GPU or TPU deployments where the all-reduce overhead is dominated by the FLOP savings. This is exactly where the MoE was designed to win, and it does.

The Gemma 4 26B MoE is, on its merits, an excellent open-weights MoE. Pick it for the workloads it was designed for. Don’t pick it because the marketing material says “more efficient” and you didn’t ask “efficient at what.”

The decision matrix

Workload Pick
Single-user local agent loop (tool use, code edit, multi-turn) 31B dense
Local long-context Q&A (one or two large reads, then short outputs) 31B dense
Local RAG service with many concurrent users 26B MoE
Local batched synthetic-data or evaluation pipelines 26B MoE
Background indexing, classification, summarization at scale 26B MoE
Fine-tuning on a narrow domain (small dataset, limited compute) 31B dense (MoE fine-tuning collapses expert specialization at small data scales)
Mech-interp / SAE / circuit work on an open MoE 26B MoE (it’s the interesting object)
Phone, Pi 5, browser deployment 4B effective (you don’t have a choice)
Cloud or multi-GPU server deployment 26B MoE (this is where it wins)

Two things to flag explicitly. First, the fine-tuning row: MoE fine-tuning is a research subfield of its own because the router and the experts have to be tuned coherently, and small datasets can collapse expert specialization. If you have 5,000 examples and want to teach Gemma 4 your codebase’s conventions, the dense model is the more forgiving target. Second, the mech-interp row: a 26B MoE with open weights is a genuinely exciting interpretability artifact — see the GemmaScope lineage on Gemma 2 dense — and if your “agent” is actually a circuit-analysis pipeline, the MoE is the more interesting object. That’s a different essay.

The benchmark you should run this weekend

Don’t take my word for any of the above. The whole point of running models locally is that you can measure. Here is the experiment, three ways, calibrated for the hardware most of you actually own.

What to measure: not tokens per second. Measure per-step p50, p95, and p99 latency across at least 200 generation calls, with prompts that approximate your real workload (short turns, mixed lengths, occasional long-context reads). If you can run an actual agent loop end-to-end, even better — record the per-step distribution and synthesize the loop-level success probability.

Apple Silicon (M2 / M3 / M4):

# Build llama.cpp with Metal backend
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp && cmake -B build -DGGML_METAL=ON && cmake --build build -j

# Community-quantized GGUFs (Google ships safetensors; unsloth ships GGUF)
huggingface-cli download unsloth/gemma-4-31B-it-GGUF 
  gemma-4-31B-it-Q4_K_M.gguf --local-dir .
huggingface-cli download unsloth/gemma-4-26B-A4B-it-GGUF 
  gemma-4-26B-A4B-it-Q4_K_M.gguf --local-dir .

# Benchmark: 200 generations of 512 tokens, log per-call timing
./build/bin/llama-bench -m gemma-4-31B-it-Q4_K_M.gguf  -n 512 -r 200 -o json > dense.json
./build/bin/llama-bench -m gemma-4-26B-A4B-it-Q4_K_M.gguf -n 512 -r 200 -o json > moe.json

The 26B-A4B suffix on the MoE is Google’s naming for “26B total params, ~4B activated per token” — the very expectation-vs-distribution gap discussed above. llama-bench gives per-call timings; pipe through jq to extract the per-call latencies and compute your own quantiles. For a richer view, MLX-LM gives cleaner kernel timing on Apple Silicon and lets you verify the dispatch overhead directly.

Consumer NVIDIA (RTX 4090, 3090, A6000):

# vLLM with the official safetensors, served on the same machine
pip install vllm
python -m vllm.entrypoints.openai.api_server 
  --model google/gemma-4-31B-it --quantization awq 
  --port 8000 &

# Drive it with a benchmark harness — 200 requests, log per-request latency
python -m vllm.benchmarks.bench_serving 
  --backend openai-chat --endpoint /v1/chat/completions 
  --base-url http://localhost:8000 
  --model google/gemma-4-31B-it 
  --num-prompts 200 --dataset-name sharegpt 
  --result-filename dense.json

vLLM’s benchmark suite reports p50/p95/p99 directly. Repeat with --model google/gemma-4-26B-A4B, compare the p99 columns, and that’s your answer.

A note: vLLM’s MoE kernel quality varies by version. Check the supported models page before assuming both architectures get equal-quality dispatch. This is itself part of the dense-by-default argument — the ecosystem is more battle-tested on dense.

Pi 5 / phone (Gemma 4 E4B):

The MoE-vs-dense decision doesn’t apply at this tier — you’re running the E4B effective-parameter model, not because you chose to, but because it’s what fits. The interesting benchmark here is different: measure token-level latency *variance* on the same prompt set across multiple runs. Edge silicon shares compute with the OS scheduler, and your real tail will be dominated by OS jitter, not architecture. Use time plus a wrapper that logs to a CSV:

# On Pi 5 with llama.cpp built for ARM
for i in $(seq 1 200); do
  /usr/bin/time -f "%e" ./build/bin/llama-cli 
    -m gemma-4-E4B-it-Q4_K_M.gguf 
    -p "$(jq -r ".prompts[$((i % 50))]" prompts.json)" 
    -n 256 -no-cnv 2>>latencies.csv
done

The lesson on the edge is the same as on the workstation: optimize for the experience the user actually has, which is the tail.

What to take away beyond this release

Gemma 4 forces a clean architectural choice at the same scale tier — dense versus MoE, 31B versus 26B. The next open release will too. The release after that. The “always pick MoE because it’s more efficient” reflex was correct for cloud training and cloud serving, and it has been pattern-matched into local deployment where it doesn’t belong.

The lens that holds up across releases is not parameter count and not FLOPs per token. It is the latency profile of your workload. If a single user is waiting on a single response chain, you want predictable tokens, not cheap tokens. If many users are sharing a pool of compute and the throughput is what’s billed, you want cheap tokens at the expense of any single user’s tail.

The open-weights ecosystem is mature enough now that you don’t have to pick on vibes. Two llama-bench runs and a quantile calculation will tell you the answer for your hardware and your prompts. Run them before you commit to an architecture.

The dense model is the default. The MoE is the optimization. Treat them in that order.

Submitted to the DEV.to Gemma 4 Write-About challenge, May 2026, with some help from AI. Code samples have been validated against the relevant tool documentation; concrete benchmark numbers are intentionally omitted because the right benchmark is your benchmark, on your hardware, with your prompts.

CodeMender Doesn’t Work Without a Skeptic

The headlines at Google I/O 2026 went where you’d expect — Gemini 3.5, the new intelligent eyewear shipping in the fall, Antigravity 2.0 with its new CLI and subagents. CodeMender — Google DeepMind’s autonomous code-security agent — got folded into Agent Platform almost as a footnote. Most coverage moved on.

That’s a mistake. CodeMender is the most architecturally significant announcement of the event, and not because it finds bugs. Tools have found bugs for decades. It’s significant because of how it claims to find them, and because of what its architecture quietly admits about where AI security is actually heading.

Read Google’s own description of how CodeMender works:

CodeMender is an AI-powered agent utilizing the advanced reasoning capabilities of our Gemini models to automatically fix critical code vulnerabilities. […] These patches are then routed to specialized “critique” agents, which act as automated peer reviewers, rigorously validating the patch for correctness, security implications and adherence to code standards before it’s proposed for final human sign-off.

Read that twice. A reasoning model proposes a patch. A separate “critique” agent — Google’s own term — sits behind it and rigorously validates before anything reaches a human. CodeMender isn’t a model. It’s a loop. And the critique agent is doing the load-bearing work.

The pitch that has been DOA for eighteen months

Every AI-security vendor since 2024 has been selling roughly the same pitch: large language models will find vulnerabilities faster than humans, with broader coverage and cheaper cost per finding. The demos look magical — a model reads a function, mumbles something about CWE-89, and produces a patch.

What the demos don’t show is what happens at scale. The genuine signal those tools surface is real and useful — that’s the upside that makes the category viable. But security analyst attention is the scarcest resource in the company. After two or three findings turn out to be noise, the analyst marks the next one as noise too — independent of whether it’s real. Trust falls off a cliff and does not amortize back. This isn’t speculative; it’s the binding constraint on any scanner’s adoption curve.

Static analyzers shipped with well-documented false-positive problems for two decades; the LLM-on-top wave inherited and often amplified them. Snyk’s DeepCode AI describes itself as combining “symbolic and generative AI, several machine learning methods, and the expertise of Snyk security researchers” — explicitly a hybrid stack, not a single model. GitHub’s Copilot Autofix “leverages the CodeQL engine, GPT-4o, and a combination of heuristics” — again, an LLM embedded in a deterministic pipeline, not a model alone. The products that struggled were the ones that shipped a single LLM with no scaffolding around it. The products that survived were the ones that figured out the scaffolding was the problem.

The model wasn’t ever the bottleneck. Trust calibration was.

What CodeMender quietly admits

The architecture description is a structural concession. Auto-patching makes the false-positive problem worse, not better — a wrong patch doesn’t just waste analyst time, it changes the codebase, breaks the build, sometimes introduces a new bug downstream. If “find a bug” is hard to ship with high enough precision, “find and fix a bug” is harder still.

You cannot solve this by training a better generator. Train the model to be more conservative and recall collapses. Train it to be more aggressive and precision collapses. The recall-precision frontier of a single model is fundamentally bounded by its prompt and its training distribution.

What you can do is split the job. One model proposes. Another model disposes. The proposer is recall-biased: it throws a wide net, surfaces everything that smells like a vulnerability, optimizes for not-missing. The critique agent is precision-biased: its job is to take each finding and try to kill it. Is this exploit actually reachable? Is there a guard upstream the proposer missed? Is this a known false-positive pattern? Does the proposed patch break existing tests?

The critique agent is the architecture. Without it, the proposer’s output is unshippable. With it, the system can claim what CodeMender does claim: that it surfaces validated patches for human review rather than a firehose of speculative ones.

That decoupling — the model that finds the bug is not the model that decides the bug is real — is the entire ballgame.

Why a two-model loop composes better than one smart model

Proposer-skeptic decomposition isn’t new. Math reasoning uses proposer-verifier loops. Multi-agent debate frames the same idea. RLHF with adversarial critics is the training-time version. Ensemble methods in classical ML have known forever that diverse weak learners beat a strong monolith on tasks with asymmetric error costs.

What’s security-specific is that the asymmetry isn’t a nice-to-have — it’s the binding constraint that makes monolithic models structurally unshippable. When error costs are asymmetric, you don’t want a single model trading off precision against recall on one axis. You want two models trading off against each other on different axes.

Four concrete payoffs from the decomposition:

  1. Different models for different roles. The discovery model can be your biggest, most expensive reasoning model, because it runs once per file. The critique model can be smaller, faster, cheaper, running many times per finding. Cost-latency becomes a tunable knob, not a constraint baked into the architecture.

  2. Independent post-training. Discover a new false-positive pattern the system keeps surfacing? You don’t retrain the giant discovery model. You add an adversarial example to the critique distribution and ship.

  3. Hot-swappable foundations. As foundation models improve, you swap them in without rebuilding the loop.

  4. Auditable disagreements. When the proposer says “vuln” and the critique says “no,” you have a structured artifact — a transcript of an argument — a human can inspect. This is the explainability story monolithic models can’t tell.

The pattern, in code

The naive single-pass setup that has powered two years of demos:

findings = discovery_model.scan(code)
return findings  # FP-saturated, analyst trust gone in two findings

The decomposed loop:

candidates = discovery_model.scan(
    code,
    prompt=DISCOVERY_PROMPT,  # recall-biased: "find anything that smells wrong"
)

validated = []
for finding in candidates:
    if finding.confidence < DISCOVERY_THRESHOLD:
        continue
    verdict = critique_model.adjudicate(
        finding,
        prompt=CRITIQUE_PROMPT,  # adversarial: "try to kill this finding"
        context=finding.surrounding_code(),
    )
    if verdict.kept and verdict.confidence > PRECISION_THRESHOLD:
        validated.append(finding.with_verdict(verdict))

patches = [patch_model.propose(f) for f in validated]
return [p for p in patches if regression_tests_pass(p)]

CodeMender — running on Google’s most capable reasoning models — still needs the critique stage. The structural decomposition does more for shippability than another order of magnitude in foundation-model parameters.

Where the convergence is

This pattern is showing up everywhere a serious security tool ships. Purdue’s LLMSCAN separates concerns explicitly: syntactic facts come from Tree-sitter parsing (verifiable, deterministic); the LLM handles only the semantic facts parsing can’t reach. The insight isn’t quite “skeptic-after-proposer” — it’s “ground the proposer in structure the LLM can’t hallucinate.” Same recall-precision frontier, attacked one tier earlier.

The RAPTOR framework goes the other direction. Candidate findings from classical pattern-matching (Semgrep, CodeQL) flow through progressive validation gates that include reachability verification via Z3 SMT solving and an exploitation-feasibility check before anything surfaces. A four-stage skeptic stack behind a recall-biased finder.

CodeMender is the most prominent expression of a pattern that has been forming quietly for over a year. The convergence is the signal.

The actual moat

Here is where the I/O 2026 announcement gets genuinely interesting, and where most takes on it will miss the point.

The architecture itself is not a moat. Anyone can stack two LLMs. The recipe is now public — Google just published it.

The moat is the critique model’s training distribution.

Think about what makes a critique agent useful in production. It is not the cleverness of the prompt. It is the catalog of adjudicated false-positive patterns the model has been exposed to — the corpus of “the proposer flagged this, a human checked it, the human said: not a vuln, here’s why” examples. That corpus is built by burning analyst trust the first time and never the second. It compounds slowly. It cannot be scraped. It is utterly specific to the languages, frameworks, and idioms of the customer base that produced it.

CodeMender, in its first six months, had upstreamed 72 security fixes to large open-source codebases, including projects of over four million lines. The output is impressive on its own. What matters more for Google’s strategic position is what those 72 fixes built: a labeled corpus of “proposed patch, critique verdict, human sign-off” tuples that nobody else has and nobody else can quickly acquire. Every accepted fix narrows the gap between Google’s critique distribution and the actual statistical landscape of real vulnerabilities in deployed code. Every rejected patch sharpens the critique’s nose for the specific shape of plausible-but-wrong fixes.

CodeMender’s moat is the critique agent’s experience, not its capability. The architecture is the cost of entry. The corpus is the durable asset.

Auto-remediation as forcing function

Here is the counterintuitive corollary. Most observers will read CodeMender as a find-and-patch tool that prioritizes auto-fix because Google wants the dramatic demo. The deeper reason is structural: auto-fix is the only forcing function that exposes whether a finding was real.

Think about the feedback signal. A patch that doesn’t compile is a confirmed false positive. A patch that compiles but breaks behavioral tests is a confirmed false positive. A patch that compiles, passes tests, but gets rejected by a maintainer with “this isn’t a security issue” is a confirmed false positive — and the maintainer’s reasoning becomes training data for the next iteration of the critique agent.

Compare this to a detection-only tool that surfaces findings to humans and asks them to triage. The feedback is delayed, ambiguous, often never logged — a JIRA ticket marked “won’t fix” doesn’t tell the model why. The find-then-fix architecture closes the loop with a far higher-fidelity signal.

Auto-remediation isn’t a UX feature riding on top of detection. It is the validation mechanism that makes detection improve over time. Tools that lean into the find-and-fix loop will out-improve tools that try to perfect detection in isolation, because they will be ingesting orders of magnitude more high-quality false-positive feedback per unit of analyst time.

What this means for the rest of the stack

A few extrapolations:

  • The unit of value in AI security is the loop, not the model. Disciplined validation with a mediocre foundation model will out-ship a state-of-the-art foundation model wrapped in a naive single-pass setup. Vendors who think the model is the product are about to get out-shipped by vendors who think the loop is the product.

  • Critique-model training data is the new ImageNet. The next round of M&A in security tooling will not be about who has the best static analyzer. It will be about who has the deepest labeled corpus of validated-and-rejected findings. The companies worth buying are the ones whose loops have been running longest with real analysts in them.

  • Open-sourcing the loop won’t kill the moat. Google could publish CodeMender’s full architecture tomorrow. The architecture isn’t the asset. The accumulated critique distribution — the millions of micro-judgments embedded after a year-plus of running against real OSS — does not transfer with the source code.

The real headline

The story buried in CodeMender’s I/O 2026 footnote is that Google — the company with the resources to build any single model it wants — has decided the path forward in code security runs through architecture and through accumulated experience, not through model capability alone. They did not announce a bigger reasoning model with better vuln-detection benchmarks. They announced a loop that has been running for over six months, learning from real upstream contributions, and getting more useful with every accepted patch.

The model is not the product. The critique agent is not the product. The corpus the critique agent has been trained on is the product. Everything else is infrastructure around that compounding asset.

Notes and sources:

How I Detect Discord Selfbots Without Reading a Single Message

The Problem

Discord selfbots – automated user accounts – are used for spam raids, phishing, and coordinated attacks. Traditional moderation bots scan message content, but that requires the MESSAGE_CONTENT privileged intent and raises privacy concerns.

I wanted to catch bots without reading what people type.

The Insight

Discord’s gateway broadcasts a TYPING_START event every time someone begins typing. This is metadata – it tells you WHO started typing, WHERE, and WHEN.

A human needs 300-500ms just to switch focus between two Discord channels. A selfbot fires parallel HTTP requests and can trigger typing in multiple channels within 5-50ms.

That gap is massive. A hard threshold of 150ms catches every bot without touching a single real user.

  Selfbot           Human
  ┌─────┐          ┌─────────────────┐
  │5-50ms│          │   300-500ms+    │
  └─────┘          └─────────────────┘
       ▲                    ▲
       │                    │
   CAUGHT              SAFE

Beyond Typing: Ghost Detection

Some selfbots disable typing events entirely (using “silent typing” plugins). So I added the inverse detection:

If a message arrives WITHOUT a preceding TYPING_START – that’s suspicious.

Normal Discord desktop clients always fire a typing event before sending. No typing + message = likely automation.

Channel Sequence Fingerprinting

Selfbots often iterate channels programmatically – by ID order (ascending or descending). Humans jump between channels randomly based on interest.

If I see typing events hitting channels in perfect numeric ID order – that’s a bot fingerprint.

The Stack

  • Rust – lock-free DashMap correlator, sub-millisecond detection
  • Twilight – lightweight Discord gateway library (not discord.js)
  • SQLite – persistent detection history, incident timeline
  • Zero privileged intents – no MESSAGE_CONTENT, no GUILD_MEMBERS, no PRESENCE

What It Cannot Do

  • Read messages (by design)
  • Catch 100% of threats (behavioral signals are probabilistic)
  • Replace Discord AutoMod (it complements it – AutoMod for content, Wiretrip for behavior)

Try It

https://wiretrip.lol – free, safe defaults (log-only mode), 30-second setup.

Notari — voice notes that never leave your phone, structured by Gemma 4

This is a submission for the Gemma 4 Challenge: Build with Gemma 4

What I Built

Notari is an Android app that records a voice note, transcribes it, and turns it into a clean, structured Markdown note — entirely on-device. The audio is held in RAM, never written to disk, and the app doesn’t even request the INTERNET permission.

I keep voice memos: meeting decisions, half-formed ideas at 11pm, reminders I’ll forget by the time I’m home. The two app categories that should solve this don’t:

  • System dictation gives me a raw transcript with no structure. I never re-read it. It rots in a folder.
  • Cloud “AI voice notes” structure beautifully, but they upload my audio — meeting decisions, personal reflections — to a server I don’t control, against a privacy policy that can change.

So I built the third option. Voice notes are the kind of content where privacy isn’t a marketing veneer — it’s a precondition for using the tool at all. If the app feels like it might leak, I won’t dictate the thing that matters most. So the privacy guarantee had to be load-bearing, not optional.

The pipeline is short on purpose:

Mic ─▶ Android SpeechRecognizer ─▶ Gemma 4 E2B (LiteRT-LM) ─▶ JSON ─▶ Room
  1. Capture. SpeechRecognizer runs in continuous-listen mode so the user can pause naturally without the recognizer giving up. The OS owns the audio buffer; the app only ever sees the text Flow.
  2. Structure. The transcript is fed to Gemma 4 E2B running locally via Google AI Edge’s LiteRT-LM runtime. The prompt is engineered to return a single JSON object — title, tags, dated mentions[], a Markdown body — and nothing else.
  3. Parse + persist. A lenient Moshi parser tolerates trailing commas and unquoted keys. On parse failure we retry once with a stricter prompt; on a second failure the transcript is saved as plain text so the user never loses content. Notes land in Room as portable Markdown.

No step touches the network. The INTERNET permission isn’t declared in the merged manifest, and a CI gate fails the build if anyone ever adds it.

Demo

Code

The privacy promise is verifiable. Run it in airplane mode. Inspect the manifest. Sniff the network. Nothing leaves the device — that’s the whole point.

How I Used Gemma 4

I chose Gemma 4 E2B (Effective-2B, INT4-quantized, ~1.5 GB on disk) running locally via LiteRT-LM. Three reasons made E2B the right fit — not E4B, not a cloud model:

  • It fits. ~1.5 GB in INT4 loads inside a 4 GB-RAM phone’s budget alongside the rest of the app. The larger Gemma 4 variant exceeded what I was willing to ship in an APK for a personal capture tool.
  • It’s strong enough for structured generation. The task isn’t open-ended reasoning — it’s “given this transcript, return a fixed-schema JSON object”. E2B does this reliably across six languages once the prompt is tuned for it.
  • LiteRT-LM ships a maintained Kotlin binding. com.google.ai.edge.litertlm:litertlm-android reads .litertlm files directly, supports GPU and CPU backends, and exposes the Engine / Session API the rest of the app is built around.

The app is opinionated: it transforms the transcript faithfully, never paraphrases meaning, and never invents dates, names, or facts. That guarantee is enforced by the prompt and verified by adversarial fixtures in core/inference/src/test/resources/prompt-eval/.

The JSON-first contract

The model is asked for one thing: a JSON object matching a fixed schema. No prose, no Markdown fences, no “Sure! Here’s the structured note:” preamble.

{
  "language": "<bcp47>",
  "title": "<short, no trailing punctuation>",
  "tags": ["<lowercase-kebab>"],
  "mentions": [
    { "surface_form": "<datetime span>", "iso_resolved": "<ISO-8601 or null>" }
  ],
  "body_markdown": "<Markdown>"
}

The prompt is versioned in core/inference/src/main/assets/prompts/structure_note_vN.txt and referenced from AssetPromptLoader.ACTIVE_PROMPT. Every change is a versioned, file-based change with a corresponding ADR. The active version is v10, evolved through ten rounds of real-corpus testing — and the evolution itself is most of what I learned about the model:

  • v2 condensed the few-shot examples after E2B started over-mimicking long examples.
  • v3 added a CURRENT TIMESTAMP block so the model could resolve “tomorrow at 3pm” to a real ISO instant.
  • v4 fixed four E2B-specific failure modes (confusing mentions[] with named entities, dropping checkboxes for spoken commitments, collapsing enumerations into prose, never using headings on multi-topic notes). The fix in every case was changing the framing from “you may” to “REQUIRED”.
  • v6 added orthographic cleanup rules (fix false starts and obvious mis-hearings without changing meaning).
  • v7 slimmed the prompt back down — removed verbose formatting-whitespace rules that were eating the cold-start prefill budget.
  • v8 added a FINAL CHECKLIST before generation and a headings-preserve-prose rule, then had to be trimmed again when the extra ~1000 characters pushed cold-start over budget on a Pixel 6a.
  • v10 fixed the most important bug of the whole project (Pillar 4): E2B was occasionally emitting the content of the worked examples as if it were the user’s note. The fix cut the examples from ten to three short, low-salience ones, replaced specific names and ticket numbers with bland placeholders, and added a blunt anti-copy guard right before the transcript. v10 also moved the language lock from the bare BCP-47 code to the language name (“English”), which stopped mixed-language titles and tags.

Robust parsing — the model will be sloppy

Even with a strict prompt, real E2B output has variance: trailing commas, occasional Markdown fences, an extra explanation after the closing brace. The parser strips any leading or trailing Markdown code fences, trims everything before the first { and after the last balanced }, and hands the cleaned slice to Moshi configured as lenient. If that fails, we retry once with a stricter RETURN JSON ONLY. NO OTHER TEXT. preamble; if that fails, we fall back to saving the raw transcript as a plain-text note. The user always keeps their content.

Audio non-persistence — the privacy backbone

The most important thing this app does is not write audio to disk. Ever. SpeechRecognizer owns the buffer; the app only ever sees a Flow<TranscriptChunk> of strings. When the user stops, awaitClose calls recognizer.destroy() and the buffer goes with it. There is no .wav, .m4a, .aac, or .tmp file in the app’s data directory at any point. The check is one line:

adb shell run-as com.voicenotemd.debug find /data/data/com.voicenotemd.debug -type f

The output lists the Room database, the DataStore settings, and the model files — and nothing audio. I verified this live during, before, and after a recording.

Backend probing — GPU first, CPU fallback

LiteRT-LM supports both Backend.GPU() and Backend.CPU(). GPU is faster on decode, but GPU init fails on some devices (the Pixel 6a’s Mali-G78 in my testing). The session factory probes GPU and recovers to CPU:

runCatching { engineFactory(Backend.GPU()) }
    .recoverCatching { engineFactory(Backend.CPU()) }
    .getOrThrow()

On the reference Pixel 6a (CPU fallback) a 1000-character note structures in ~50-60s; on a device that gets the GPU path it’s ~15-25s.

Engine lifecycle — keeping 1.5 GB livable

The engine is ~1.5 GB resident — most of a 4 GB device’s budget. LiteRtLmGemmaSession implements ComponentCallbacks2 and releases the engine on onTrimMemory(TRIM_MEMORY_BACKGROUND), reloading lazily. To hide cold-start, warmUp() is fire-and-forget from CaptureViewModel.init — by the time the user has tapped the mic and started talking, the engine is already loading.

Multilingual handling

The prompt detects the input language and produces the title, tags, body, and datetime surface forms in that language. Datetimes resolve against the device timezone, so “domani alle 15”, “tomorrow at 3pm”, and “mañana a las 3” all produce real ISO instants. Supported at v1: English, Italian, Spanish, French, German, Portuguese. The UI is English-only in v1 — UI localization is a roadmap item.

Interoperability — your notes are not hostages

Every note is, by construction, a portable Markdown file with YAML frontmatter (Note.toMarkdownWithFrontmatter()). Drop it into an Obsidian vault, a Logseq graph, or any folder you sync — tags, resolved datetimes, headings, and checkboxes all carry with it. The privacy promise isn’t just “we don’t send your data”, it’s “your data was always yours”.

What I learned about Gemma 4 E2B

Framing matters more than I expected. Going from “use checkboxes for tasks” to “REQUIRED: every I need to / must is a - [ ] checkbox” was the single largest quality jump. E2B respects directives far more reliably than permissions.

Few-shot examples are tokens, not magic — and they can leak. Early prompts had eight to ten examples; E2B over-mimicked their length and, worse, sometimes copied their content into the user’s note. Cutting to three short, low-salience examples fixed both the bloat and the leakage. This was the scariest bug of the project precisely because it violated the core “transform, don’t invent” promise.

Schema is the strongest hint. An inline schema block plus three worked examples beats every “be sure to return valid JSON” instruction. The strict-retry pass works because it isn’t asking for new content — just restating the schema with louder caps locks.

It can do temporal reasoning if you give it the time. Without CURRENT TIMESTAMP in the prompt, every relative date came back null. With it, ~95% of relative dates resolve correctly across the six languages.

It can’t be a fact source. Anything that requires recall — “the dentist I always go to” — is hallucination territory. The contract is transform, never augment, and I verify it with adversarial fixtures.

Latency is real but tameable. ~60s on a Pixel 6a (CPU) sounds long until you remember the user just spent 60 seconds dictating. Pre-warming the engine and showing a clear progress affordance turns it into “I see something happening” rather than “is this frozen?”. On the GPU path it’s ~15-25s.

What’s next

Three upgrades I deliberately cut from v1 to ship within the competition window:

  • Gemma audio-native ASR. Replace SpeechRecognizer with Gemma 4 E2B’s multimodal audio input so transcription, language detection, and structuring all happen in one forward pass.
  • Tool calls for calendar. With Gemma function-calling, a resolved mentions[] could surface an on-device “Add to calendar” affordance via Intent.ACTION_INSERT. Still no network.
  • Ask your past notes — local RAG. A small on-device embedding model (INT8, well under 200 MB) would make the corpus searchable by meaning. Embeddings live in Room next to the notes; queries never leave the device. This is roadmap and not v1 because RAG needs careful citation handling to keep the “transform, don’t augment” promise.

Notari was built solo across the two weeks of the Gemma 4 Challenge. The model file is downloaded once, manually, from Google AI — no analytics, no telemetry, no surprises. The name takes its cue from the Latin notarius — the historically trusted recorder of spoken statements. That, in two syllables, is the product.

Hexapod agent powered by Gemma4:e4b

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

Gemma4:e4b Hexapod Robot Project

What I Built

I built an AI-powered Hexapod Robot capable of autonomous navigation and dynamic gait adjustment. The project solves the complexity of coordinating 18 servos to maintain stability across uneven terrain, creating an experience where the robot can ‘reason’ about its movement based on sensor feedback rather than relying on hard-coded patterns.

Code

Iron-Hermes Agent Client Code

Server Code (Unmodified)


How I Used Gemma 4

I utilized the gemma4:e4b model. I chose the e4b model because it fits locally on my Mac Mini, yet provides the sophisticated spatial reasoning and logic precision required to translate high-level commands (e.g., “navigate to the red object”) into explicit, coordinate-based gait adjustments and parameter generation.

System Architecture & Communication

The hexapod server operates across 2 dedicated ports:

  • Port 5000: Command Control (TCP)
  • Port 8000: Video Transmission

Operational Workflow

1. The Initial Command (Action)

  • Trigger: The user issues a command instructing the agent to move forward and stop when it detects an object in close proximity.
  • Decision: The Large Language Model (LLM) determines it is safe and time to initiate movement.
  • Tool Called: hexapod_tcp.rs
  • Execution: The agent invokes the TCP tool with the exact string protocol required by the robot’s onboard hardware to initiate forward movement (e.g., CMD_MOVE#FORWARD#SPEED50). The robot physically begins walking.

2. The Sensory Polling Loop (Perception)

Because LLMs do not possess a continuous, constant “stream” of consciousness, the agent actively polls its environment at regular intervals while the robot is in motion by repeatedly executing its sensory tools:

  • Imaging via hexapod_video.rs: The agent executes this tool to capture the latest frame from the onboard camera (protected via an ENV_LOCK). The model analyzes the image matrix to determine whether a visual obstruction exists, such as a wall or a drop-off.
  • Sonar via hexapod_tcp.rs: To acquire precise, real-time distance data, the agent dispatches a telemetry request over the open TCP socket (e.g., GET_SONAR_DIST). The tool awaits the hardware return payload, receives the integer distance data (e.g., 15cm), and feeds that string back into the LLM context window for subsequent action evaluation.

I Build ML Infrastructure for a Living — Here’s Why Hermes Agent Changes the Game for Platform Engineers

Hermes Agent Challenge Submission: Write About Hermes Agent Challenge Page

I’ve spent the past year building NeuroScale — an open-source AI inference platform on Kubernetes. 108 commits. 21 automated smoke checks across 6 milestones. The kind of platform where a developer fills in a Backstage form and gets a production-grade inference endpoint with drift control, policy guardrails, and cost attribution — no kubectl required.

I’m telling you this because I need you to understand where I’m coming from when I say: Hermes Agent isn’t just another AI coding assistant. It’s the first agent framework that actually thinks like a platform engineer.

I don’t say that lightly.

The Problem Nobody Talks About: AI Agents Are Stateless in a Stateful World

Building ML infrastructure teaches you one thing fast: everything is state.

Your ArgoCD sync status is state. Your Kyverno policy violations are state. The drift between what’s in Git and what’s running in the cluster — state. The fact that someone ran kubectl apply directly at 2am and broke the GitOps contract — that’s state too.

Every AI agent I’ve used before Hermes treats each conversation like a blank canvas. You explain your architecture. You describe the problem. You get a plausible answer. Then you close the tab and do it all over again tomorrow.

Groundhog Day for infrastructure debugging.

Hermes Agent is architecturally different, and the difference matters specifically for the kind of work platform engineers do.

Three-Layer Memory: What It Actually Means for Infrastructure

Most people writing about Hermes focus on the memory system as a convenience feature. “It remembers your preferences.” “It knows your name.”

That’s not what makes it interesting.

Hermes runs a three-layer memory architecture:

  • Short-term — current conversation context (same as every other agent)
  • Medium-term — session summaries that persist between conversations, built through periodic “memory nudges”
  • Long-term — Skill Documents that capture how it solved specific types of problems, stored as reusable procedures

For a platform engineer, this maps directly to something we already understand: runbooks.

When I troubleshoot an ArgoCD sync failure, I don’t start from first principles. I check the runbook. Token expiry? Webhook misconfiguration? Sync wave ordering? The runbook encodes prior incident resolution as a procedure.

Hermes does this automatically. After roughly 15 tasks, its GEPA loop (Goal → Execute → self-Prompted introspection → Adapt — published at ICLR 2026 as an Oral) kicks in: it reviews its own performance, identifies patterns, and writes new Skill Documents. Agents with 20+ self-generated skills complete similar future tasks 40% faster than fresh instances.

That’s not “remembering your name.” That’s an agent building its own runbook library. It’s the difference between a junior on-call engineer and a senior who’s seen every failure mode before.

Where Hermes Creates Real Value in an ML Platform Stack

Abstract possibilities are cheap. Let me be specific about where this matters in a stack like NeuroScale.

1. Configuration Drift Diagnosis

NeuroScale uses ArgoCD with selfHeal: true — drift is auto-corrected. But detecting drift before ArgoCD catches it, and understanding why it happened, is a different problem.

Here’s what a Hermes scheduled audit looks like in practice:

hermes task add --cron "0 */6 * * *" 
  "Check the diff between Git-declared state in infrastructure/apps/ 
   and live cluster state. If they diverge, summarize what changed, 
   correlate with recent kubectl audit logs, and flag whether the 
   change was human-initiated or a controller reconciliation. 
   Send results to Telegram."

Most agents can run a diff. Hermes does the part that matters: building a pattern library over time. After a month of audits, it knows that drift in the serving-stack namespace is almost always a Knative autoscaler update (harmless), while drift in kyverno/policies/ is almost always someone bypassing admission control (critical).

That context accumulates in Skill Documents. I haven’t seen another agent framework that does this out of the box.

Here’s what a drift report from Hermes actually looks like after a few weeks of accumulated context:

📋 Drift Audit — 2026-05-23 12:00 UTC

Cluster: neuroscale-prod
Namespaces scanned: 4

✅ serving-stack: 2 diffs detected
   → Both are Knative autoscaler reconciliations (harmless)
   → Matches pattern from Skill: "knative-autoscaler-drift"
   → No action required.

⚠️ kyverno/policies: 1 diff detected
   → ClusterPolicy "require-resource-limits" modified in-cluster
   → Not present in Git (infrastructure/policies/)
   → kubectl audit: manual apply by user "ops-admin" at 03:12 UTC
   → FLAGGED: Possible admission control bypass.
   → Recommend: Revert in-cluster change or commit to Git.

📎 Context: This is the 3rd manual policy edit in 14 days.
   Previous incidents resolved by reverting. See Skill:
   "kyverno-drift-response" for standard procedure.

Notice the last three lines. That’s not a generic diff. That’s an agent referencing its own operational history — correlating today’s anomaly with patterns it learned from previous audits. A fresh agent instance can’t do that. One with a month of Skill Documents can.

2. Policy Validation Before Merge

NeuroScale enforces 5 Kyverno ClusterPolicies — requiring resource limits, standard labels, non-root containers, no :latest tags. But violations caught at admission mean the deploy already failed. The earlier you catch them, the cheaper the fix.

This is where Skill Documents become genuinely powerful. You write one that encodes your specific policies:

# Skill: NeuroScale Policy Pre-Check
## When to Use
When reviewing PRs that modify files under `apps/` or `infrastructure/`.
## Procedure
1. Check for `owner` and `cost-center` labels on all InferenceService manifests
2. Verify `resources.requests` and `resources.limits` are set
3. Flag any image tag that is `latest` or missing
4. Verify `securityContext.runAsNonRoot: true`
## Known False Positives
- ClusterServingRuntime objects are exempt from label requirements

That’s not a prompt. It’s a procedural memory document — loaded on-demand, zero tokens until needed, self-improving based on new violations it discovers.

3. Incident Response That Compounds

Real scenario from NeuroScale development: Backstage went into a CrashLoop. Root cause was a token refresh issue with the Kubernetes service account. I documented it in INCIDENT_BACKSTAGE_CRASHLOOP_RCA.md.

With Hermes running persistently — which you can do on a $5 VPS or a serverless backend that hibernates when idle — it would have:

  • Detected the CrashLoop via scheduled health check
  • Correlated with recent changes (cert rotation? secret update?)
  • Checked its Skill Documents for prior Backstage incidents
  • Either resolved it or escalated with a structured diagnosis

Next time a similar issue occurs, it resolves faster because the skill from incident #1 already exists. That’s the compounding effect that makes experienced SREs more valuable over time — now encoded in an agent’s memory.

What Hermes Gets Right That Other Frameworks Don’t

I’ve looked at the landscape — LangChain, CrewAI, AutoGen. Here’s what Hermes gets structurally right for infrastructure:

Local-first data residency. Everything lives in a local SQLite database. For platform engineers working with cluster credentials and deployment configs, this isn’t a feature — it’s a prerequisite. I’m not sending my policy violations through someone else’s API.

Terminal backends that work. Seven backends including local, Docker, SSH, and serverless options. SSH means Hermes runs commands on your actual infrastructure. Docker means you can sandbox it. Serverless means it hibernates when idle, wakes on demand. This is infrastructure-native thinking, not “here’s a chat UI that can run Python.”

Built-in cron scheduling. Natural-language-configured scheduled tasks with delivery to Telegram, Discord, Slack, or Signal. For infrastructure monitoring, this is table stakes — and Hermes is one of the few agent frameworks that ships it natively, no external cron daemon or YAML required.

200+ model support. Switch between cheap models for routine audits and powerful ones for complex diagnosis with a single command. No code changes. Operational flexibility that platform engineers actually need.

What Hermes Doesn’t Solve (Yet)

Honesty about limitations matters more than hype when we’re talking about tools that touch production infrastructure.

Domain reasoning is shallow. Hermes can follow procedures and build skill documents, but it can’t replace a senior engineer’s intuition about why a particular autoscaler configuration causes cascading latency under specific traffic patterns. The skill system captures what to do, not why it works.

Multi-cluster coordination is manual. NeuroScale runs on a single cluster. For federated infrastructure across regions, Hermes’ per-instance memory doesn’t federate. Each agent builds its own skill library independently. There’s no skill-sharing protocol between agents yet.

Approval workflows need hardening. The --yolo flag bypasses all approval prompts. For infrastructure work, that’s terrifying. The approval system needs declarative rules about what the agent can and cannot do — something like Kyverno’s admission policies, not just per-command approve/deny. The tools/ directory has approval pinning in progress, but it’s not production-ready for high-stakes operations.

The Bigger Picture: Agents as Infrastructure Primitives

Here’s the perspective I haven’t seen anyone else articulate.

Hermes Agent isn’t just a tool for platform engineers. It’s a new kind of infrastructure primitive.

Think about the trajectory: manual server management → configuration management → infrastructure as code → GitOps → platform engineering. Each layer abstracted the layer below and added intelligence.

Hermes represents the next step: infrastructure as conversation. Not in the shallow “chat with your cluster” sense. In the sense that an agent with persistent memory, self-improving procedures, and scheduled automation can become a layer in your control plane.

A layer that:

  • Observes continuously (cron + terminal access)
  • Learns from incidents (GEPA → Skill Documents)
  • Enforces patterns (skill-driven validation)
  • Communicates across channels (Telegram/Slack/Discord)
  • Costs almost nothing when idle (serverless backends)

That’s not a chatbot. That’s an operator — in the Kubernetes sense of the word.

Why I’m Betting on Hermes

The tools that win aren’t the ones with the most features. They’re the ones with the right architecture for compounding.

ArgoCD won over manual deploys because GitOps compounds — every deployment is auditable, reproducible, reversible. Kyverno won over manual policy checks because admission policies compound — every new policy protects every future deployment.

Hermes Agent’s architecture compounds the same way. Every task makes it better at the next one. Every incident resolution becomes a skill document. Every audit pattern becomes a scheduled automation.

164,000 GitHub stars in under three months. MIT licensed. Runs on a $5 VPS. Data stays on your machine.

For platform engineers who’ve spent years building systems that self-heal, self-monitor, and self-govern — Hermes Agent is the first AI framework that actually speaks our language.

I’m Sodiq, and I build ML infrastructure platforms. NeuroScale is open source: github.com/sodiq-code/neuroscale-platform — PRs welcome. If you want to see how Hermes Agent could fit into a real Kubernetes-based ML platform, that’s where I’d start.

Star the repo if this perspective was useful. And if you’ve tried Hermes against your own infrastructure — what broke first? I want to know.

# Introduction to Machine Learning: How We Arrive at Linear Regression

Before we talk about Linear Regression, we first need to understand the bigger idea it belongs to Machine Learning.

Machine Learning is the reason applications today can:

  • recommend movies on Netflix,
  • suggest products on Amazon,
  • recognize faces on your phone,
  • and even predict house prices or exam scores.

But what exactly is Machine Learning?

What is Machine Learning?

Machine Learning is a branch of Artificial Intelligence where we teach computers to learn patterns from data instead of explicitly programming every rule.

Traditional Programming vs Machine Learning

In traditional programming:

You give the computer rules + data → it gives you answers.

In Machine Learning:

You give the computer data + answers → it learns the rules.

Simple Analogy

Think of teaching a child:

Traditional programming:

You say:

  • “If you see 2 + 2, always answer 4.”

You must manually define every rule.

Machine Learning:

You show the child many examples:

  • 1 + 1 = 2
  • 2 + 2 = 4
  • 3 + 3 = 6

Eventually, the child learns the pattern:

“Oh… adding numbers follows a pattern.”

That is exactly how Machine Learning works.

The Goal of Machine Learning

The main goal is simple:

To help machines learn patterns from data and make predictions on new, unseen data.

Types of Machine Learning

There are three main types:

1. Supervised Learning

The model learns from:

  • input (data)
  • output (correct answers)

Example:

  • house size → house price
  • study hours → exam score

This is where Linear Regression belongs.

2. Unsupervised Learning

The model is given data without answers and tries to find patterns on its own.

Example:

  • grouping customers by behavior
  • clustering similar items together

3. Reinforcement Learning

The model learns through:

  • rewards
  • mistakes
  • trial and error

Example:

  • game-playing AI
  • robotics navigation

From Machine Learning to Prediction Problems

Once we focus on supervised learning, we usually ask questions like:

  • “Can we predict a number?”
  • “Can we estimate a value?”
  • “Can we forecast future outcomes?”

These are called regression problems.

What is a Regression Problem?

A regression problem is when we try to predict a continuous numerical value.

Examples:

  • house price (e.g., 150,000)
  • temperature (e.g., 28°C)
  • exam score (e.g., 75%)

This is different from classification, where we predict categories like:

  • yes/no
  • spam/not spam
  • dog/cat

Enter Linear Regression

Now that we understand regression problems, we can introduce one of the simplest solutions:

Linear Regression

Linear Regression is a supervised learning algorithm used to predict continuous values by finding a relationship between input and output variables.

Why Linear Regression?

Because many real-world relationships can be approximated using a straight line.

Example:

  • More study hours → higher exam scores
  • Bigger houses → higher prices
  • More advertising → more sales

These relationships often follow a pattern that can be simplified as:

“As X increases, Y also increases (or decreases) in a predictable way.”

The Core Idea of Linear Regression

Linear Regression tries to draw a best-fit line through data points.

This line is used to:

  • understand patterns
  • and make predictions

Mathematically, it is written as:

y = mx + c

Next we will do a deep dive into Linear Regression; Buckle up!

Google I/O 2026 Told Me AI Can Help Learners — So I Built an Entire Skill System for My Master’s Degree

This is a submission for the Google I/O Writing Challenge

The Moment That Stopped Me Mid-Scroll

I was half-watching the Google I/O 2026 keynote during a break between lectures when something Sundar Pichai said made me put my coffee down. It was during the opening segment — Google’s vision for AI assisting educators and learners. Not replacing them. Assisting them.

That hit differently for me because I’m not a developer by training. I’m a Public Health Biotechnology master’s student, drowning in epidemiology formulas, statistical tests I can never remember the assumptions for, and primer design rules that change depending on whether I’m doing standard PCR or qPCR.

Then I switched to the Developer Keynote, and Josh Woodward started talking about building agents — not just chatbots, but agentic systems that reason, use tools, and collaborate. The session “Build agents with Gemini API” talked about going from prompt to production agent with the Gemini Interactions API. The “Future of Software Development” session described how “coding is evolving from writing boilerplate to curating outcomes.”

And I thought: what if I’m not just a consumer of AI tools — what if I build the tools that help me learn?

So I did. And Google’s own architecture validated every decision I made.

What Google Announced (and Why a Biotech Student Cared)

Let me ground this in the specific I/O 2026 announcements that matter here:

1. The Agentic Paradigm Shift

Google I/O 2026 made one thing abundantly clear: we’ve moved from “prompt → response” to “agent → autonomous reasoning → action.” The Gemini API now supports managed agents — a single API call provisions a Linux sandbox where the agent reasons, executes code, manages files, and browses the web autonomously1.

But the announcement that made me sit up was Google’s Agent Development Kit (ADK) 2.0 going GA with graph workflows and collaborative agents2. ADK introduced a concept called Skills — “self-contained units of functionality that an agent can use to perform a specific task”3. Skills follow a three-level progressive loading architecture:

  • L1 (Metadata): Skill name and description for discovery
  • L2 (Instructions): Primary procedure, loaded when triggered
  • L3 (Resources): Reference materials, loaded on demand

Sound familiar? It should. Because I independently built the exact same architecture for my coursework — and I was doing it before ADK 2.0 was announced.

2. AI for Learners, Not Just Developers

The Google Keynote’s framing of AI as a tool that “enhances creativity, extends knowledge, and advances science”4 wasn’t just corporate messaging to me. It was a description of my Tuesday afternoon. I use AI to understand disease mechanisms, check my statistical reasoning, and generate structured slides for seminars. The difference is: I got tired of explaining my field to the AI every. single. time.

3. From Vibe Coding to Vibe Learning

The “Build next-gen AI experiences with Google AI Studio and Google Antigravity” session introduced Google’s agent-first IDE where you “speed-run ideas in AI Studio, export code, and deploy agents to handle heavy lifting like planning architecture [and] writing multi-file features.”5

I realised: what if the “heavy lifting” isn’t code architecture — what if it’s the cognitive architecture of a master’s programme?

What I Actually Built

I built pub-health-biotech-skills — a modular repository of reusable AI skills, expert agents, and one-shot prompts covering my entire MSc curriculum. It follows the same L1/L2/L3 progressive loading pattern that Google’s ADK now formalises as a specification6.

Here’s the architecture:

pub-health-biotech-skills/
├── skills/              # Domain-specific workflows (progressive loading)
│   ├── epi-study-design/
│   │   ├── SKILL.md         # L2: Procedure + triggers
│   │   └── references/      # L3: Loaded on demand
│   │       ├── study-types.md
│   │       └── bias-checklist.md
│   ├── biostat-test-selector/
│   │   ├── SKILL.md
│   │   └── references/
│   │       ├── decision-tree.md
│   │       └── assumptions.md
│   ├── molgen-primer-design/
│   ├── genetics-pedigree/
│   └── ... (13 skills total)
├── agents/              # Expert persona agents
│   ├── epidemiologist.agent.md
│   ├── biostatistician.agent.md
│   ├── geneticist.agent.md
│   └── molecular-biologist.agent.md
├── prompts/             # One-shot task templates
└── docs/                # Cross-LLM compatibility

Three Layers, Three Purposes

Skills are workflows. My biostat-test-selector skill doesn’t just say “help with stats.” It contains a step-by-step decision tree: ask the outcome type → check number of groups → determine independence → verify assumptions → recommend a test → provide the R code → give the APA reporting template. The reference files hold the actual formulas and assumption checklists — loaded only when needed.

Agents are expert personas with hard constraints. My @biostatistician refuses to use stepwise selection for causal models, won’t interpret p > 0.05 as “no effect,” and always reports effect sizes alongside p-values. My @epidemiologist won’t let you calculate a relative risk from a case-control study. These aren’t stylistic preferences — they prevent the kinds of errors that cost you marks on an exam.

Prompts are one-shot commands. Type /critique-study, paste the paper, get a structured evaluation against CARE or STROBE guidelines with scoring.

The Google I/O Connection: Why This Architecture Works

Here’s what Google’s announcements validated:

Progressive Loading Solves the Context Problem

ADK’s documentation states that skills help “optimize the context window by only loading instructions when they are needed”3. I learned this the hard way. My first attempt loaded everything — all formulas, all decision trees, all guidelines — into every conversation. The AI’s responses got worse. It was juggling Hardy-Weinberg equilibrium math while I was asking about study design bias.

Splitting into skill-specific references with on-demand loading fixed response quality immediately. Google formalised this as L1/L2/L3. I call it “not overwhelming the AI with my entire curriculum.”

Multi-Agent Workflows Are the Future

ADK 2.0’s collaborative agent workflows2 mirror what I’m building toward: chain the biostatistician and epidemiologist together to draft a complete methods section. Design → sample size calculation → analysis plan → reporting template, all in one flow. Google’s architecture proves this isn’t a hack — it’s the direction the industry is heading.

Skills Are Reusable Across Models

The “What’s New in Google AI” session showcased building with the full Gemini model family. My skills are model-agnostic markdown files. They work in VS Code with GitHub Copilot, in Claude Projects, as ChatGPT custom instructions, or as system prompts for Gemini. The knowledge doesn’t care what model reads it — just like ADK supports Gemini, Gemma, Claude, Ollama, and others7.

A Real Workflow: Exam Revision at 11pm

It’s the night before my biostatistics exam. I need to revise statistical test selection.

Without skills:

“You are a biostatistician. I have a continuous outcome, 3 independent groups, small sample size, probably not normally distributed. Which test should I use? Walk me through the decision process, check assumptions, give me R code, and format it in APA style. Also show the non-parametric alternative in case assumptions are violated. And don’t forget to mention post-hoc tests…”

With skills:

/biostat-test-selector

“Continuous outcome, 3 groups, n=15 each, non-normal”

The AI already knows the decision tree. It walks through it step by step. Recommends Kruskal-Wallis. Gives me the R syntax. Shows the post-hoc pairwise comparison approach. Provides an APA reporting template. 30 seconds, done.

That’s not a time saving. That’s a cognitive saving. At 11pm before an exam, the last thing I need is to spend mental energy crafting the perfect prompt.

My Honest Critique: What Google Got Right and What’s Missing

What They Got Right

The Skills specification is brilliant. The three-level loading (metadata → instructions → resources) with the SKILL.md + references/ structure is exactly what domain-heavy applications need. It’s not over-engineered — a SKILL.md file is just markdown with YAML frontmatter. Anyone can write one.

ADK is model-agnostic by design. Supporting Gemini, Claude, Ollama, and LiteLLM means skills aren’t locked into Google’s ecosystem. This matters for students who can’t always pay for API access.

The emphasis on “curating outcomes” over “writing code” in the developer keynote resonates. I’m not an engineer. I’m a public health student who happens to know enough markdown to build an agent system. The barrier to entry is low, and that’s intentional.

What’s Missing (for learners like me)

No education-specific examples. Every agent demo at I/O was about travel booking, logistics, or enterprise. Where are the agents that help students learn? The architecture supports it perfectly — Google just hasn’t shown it.

Skills discovery is still primitive. ADK Skills are triggered by description matching. For a student with 13 skills across 4 domains, the AI sometimes picks the wrong one. I want curriculum-aware routing: “I’m studying for my Epi Methods exam” → automatically surface only epidemiology skills.

No built-in validation against ground truth. When my @biostatistician agent recommends a test, I trust it because I wrote the decision tree from my textbook. But there’s no mechanism to verify outputs against known-correct answers. For educational use, this matters more than for enterprise use.

Five Lessons for Anyone Building Learning-Focused Agents

  1. Constraints produce better output than instructions. Telling my agent “do NOT interpret p > 0.05 as no effect” prevents more errors than “always interpret carefully.” Google’s managed agents use similar constraint patterns in their safety documentation1.

  2. Structure beats length. A concise decision tree outperforms a 2,000-word system prompt. The AI follows procedures better than prose. ADK’s skill structure enforces this with the L2/L3 separation.

  3. Domain knowledge is the moat. The SKILL.md is routing logic. The reference files — the formula sheets, the bias checklists, the assumption verification steps — that’s where the real value lives. Anyone can build an agent. Few people will curate domain-specific knowledge with enough rigour to be trustworthy.

  4. Agent personas need opinions. A generic “statistics helper” gives wishy-washy answers. A biostatistician who insists on confidence intervals and refuses stepwise regression gives answers you can actually submit. Give your agents convictions.

  5. Build for the model you’ll use tomorrow, not today. My skills are markdown files with no vendor lock-in. When Gemini 3.5 Flash drops in price or a new open-source model appears, I just point it at the same skill files. Google’s multi-model support in ADK confirms this is the right call7.

What’s Next: The Roadmap

This project started as a productivity hack. After watching I/O 2026, I see it as something bigger.

Near-term: More reporting guideline skills (STROBE for observational studies, CONSORT for RCTs, PRISMA for systematic reviews). An exam revision mode that generates practice questions from lecture notes and marks my answers.

Medium-term: Multi-agent workflows using the patterns ADK 2.0 formalised — chain the epidemiologist and biostatistician for complete methods section drafting. Integration with PubMed for evidence-grounded responses. Interactive R notebooks that execute analyses, not just suggest code.

Long-term: Open this to other health science students. Immunology skills. Pharmacology skills. Nursing research methods. A community-contributed skill library where every student’s domain expertise benefits everyone else — the same collaborative model Google is pushing with their open-source ADK repositories.

The Bigger Picture

Google I/O 2026’s message was clear: we’re moving from AI as a chatbot to AI as an autonomous collaborator. The tools are here — Gemini Interactions API, managed agents, ADK Skills, agent-first IDEs.

But the most underrated insight from the entire event wasn’t technical. It was philosophical. In the keynote intro, when they talked about AI helping educators and learners, they weren’t announcing a product. They were acknowledging a use case that developers often overlook: the people who need AI most aren’t building software — they’re building knowledge.

I’m a public health student. My “production environment” is an exam hall. My “deployment target” is a research proposal. My “users” are my supervisors who will read my thesis. And the agents I built help me meet those targets with more rigour, more consistency, and more confidence than prompting from scratch ever could.

Google gave us the architecture. The question is: what will you build with it that isn’t another to-do app?

References

Disclosure: This article was written with assistance from GitHub Copilot (Claude Opus 4.6) for drafting and structural editing. All domain knowledge, project architecture, and opinions are my own. The project described is real and available on GitHub.

If you’re a student building something similar for your field — or if you’re a developer who’s never thought about AI for education — I’d love to hear your take in the comments.

  1. Google. “Agents Overview — Gemini API.” Google AI for Developers, 19 May 2026. https://ai.google.dev/gemini-api/docs/agents ↩

  2. Google. “ADK Python 2.0 GA — Graph Workflows and Collaborative Agents.” Agent Development Kit Documentation, 2026. https://adk.dev/2.0/ ↩

  3. Google. “Skills for ADK Agents.” Agent Development Kit Documentation, 2026. https://adk.dev/skills/ ↩

  4. Google. “Google I/O 2026 Writing Challenge — Sponsored by Google AI.” DEV Community, May 2026. https://dev.to/challenges/google-io-writing-2026-05-19 ↩

  5. Google. “Build next-gen AI experiences with Google AI Studio and Google Antigravity.” Google I/O 2026 Session, May 2026. https://io.google/2026/explore/pa-keynote-2 ↩

  6. Agent Skills Specification. https://agentskills.io/specification ↩

  7. Google. “Agents — Models for Agents.” Agent Development Kit Documentation, 2026. https://adk.dev/agents/models/ ↩