WRITING
Date
June 2026
Read time
9 min
Category
engineering

Five States, Not Two — Why Most AI Tools Treat Their Users Like an Afterthought

Most builders design for success and bolt on an error page. I designed five explicit states. Here's why the failures are the product.

I turned off the safety system.

Not by accident. Not because it failed. I turned it off because it was causing more damage than it prevented — and I could prove it.

The production system I built — an autonomous agent platform with multiple specialized agents operating across three execution tiers — included a protection layer designed to prevent unsafe operations. Layer 4 in a five-layer safety architecture. Its job was to intercept agent actions that modified critical files and require confirmation before proceeding.

Scene: Control room with a deliberate hand turning off a safety switch, calculated not reckless

In theory, this was prudent engineering. In practice, it was a bottleneck that turned every routine file update into a two-step confirmation dance. Agents that needed to update configuration files — a daily operation, part of normal system maintenance — would hit the gate, pause, wait for confirmation, resume. The delay cascaded. Downstream agents that depended on the updated configuration would timeout waiting for data that was sitting behind a confirmation dialog nobody was watching.

I ran the numbers. Protection-to-disruption ratio: negative. The safety layer was preventing zero actual unsafe operations — no agent had ever attempted a genuinely dangerous file modification — while blocking dozens of legitimate ones daily. The cost of the protection exceeded the cost of the risk it was designed to prevent.

So I disabled it. But not casually. I wrote a decision record that documented five things: what I was disabling, why the protection-to-disruption ratio justified it, what I was giving up (automated interception of unsafe file operations), what compensating control replaced it (audit logging with post-hoc review), and what would trigger re-enabling it (any incident where an agent modified a file it shouldn’t have).

Naming what you gave up is the discipline. If you can't fill in that field, you haven't finished the decision.


That decision record answered a question three months later — in a link, not a thirty-minute meeting. A new component was being added to the system. Someone asked whether the safety layer should intercept the new component’s file operations. The decision record explained why the layer was disabled, what would trigger re-enabling it, and what compensating controls existed. The question was answered in the time it took to read a document.

This is what I mean by architectural intentionality. Not “we made good decisions” — everyone thinks they make good decisions. Intentionality means every structural choice has a documented rationale, including what was rejected, what was given up, and what would change the decision.

The Ceremony Was the Price

A different project crystallized this from the opposite direction. I was refactoring a notification processing system for an enterprise B2B platform — a monolithic service class that had grown to 600 lines. It handled database queries, template rendering, email delivery, schedule management, and error handling in a single file with deeply nested conditionals.

I decomposed it into five services. Query service. Template service. Delivery service. Schedule service. Error handler. Each with a single responsibility, its own test surface, and explicit contracts at every boundary.

The alternative I rejected was a mediator pattern — a lightweight orchestrator that would coordinate the existing code blocks without restructuring them. The mediator would have been faster to implement, preserved the existing code paths, and reduced the immediate risk of refactoring regressions.

I rejected it because it would have preserved the testability problem. The monolith was untestable not because it was big, but because the responsibilities were entangled. You couldn’t test email delivery without triggering schedule updates. You couldn’t test template rendering without connecting to the database. The mediator would have organized the entanglement without eliminating it.

What I gave up: simplicity. The decomposed architecture has more files, more interfaces, more ceremony at every boundary. A developer who opens the project for the first time sees five services where there used to be one. The learning curve is steeper. The ceremony is the price of testability, and I documented that tradeoff explicitly: “We chose structural complexity over operational opacity because debugging a five-service pipeline with clear contracts is faster than debugging a single class where every method has invisible side effects.”


An infrastructure decision on a different project forced me to confront when intentionality means choosing less.

I was building deployment automation for a Windows installer project. The conventional approach would be a CI/CD pipeline — GitHub Actions watching the repository, building the MSI on every push, running tests, deploying artifacts. Standard. Resume-worthy. The kind of infrastructure that portfolio reviews expect to see.

I built local PowerShell scripts instead.

When CI/CD Is the Wrong Answer

The project had one developer. One repository. One build target. The CI/CD pipeline would require configuring a GitHub-hosted Windows runner, managing Personal Access Token rotation, handling artifact storage, and maintaining the pipeline YAML — ongoing operational overhead for a system that builds maybe twice a week.

A local build script runs in seconds, requires zero authentication infrastructure, and has no maintenance burden between builds. The CI/CD pipeline saves three minutes per build and costs hours of token management and runner debugging per quarter. The economics don’t work for a single-developer project with a low build frequency.

What I gave up: automated builds on push, artifact versioning in the cloud, the ability to build from any machine without local tooling, and the portfolio optic of “this project has CI/CD.” What I gained: zero operational overhead, instant builds, and no PAT rotation anxiety.

I documented the escape hatch: “When a second developer joins this project, or when build frequency exceeds twice daily, revisit this decision and implement GitHub Actions with a self-hosted Windows runner.” The trigger conditions are specific. The migration path is documented. The decision is reversible, and the decision record says exactly when to reverse it.


This is the same discipline that shaped the Comprehension Audit’s frontend architecture — and it’s why the tool has five states instead of two.

Most AI tools have two states: the form works, or something went wrong. The happy path gets the design attention. The failure path gets a generic error modal — red text, an apology, and a “try again” button that does the same thing and fails the same way.

The Comprehension Audit manages five explicit states, each designed as a first-class experience:

The form state — four questions, clean layout, contextual guidance for each question. This is the state most builders start and stop with.

The loading state — and this one matters more than most builders realize. The LLM judge takes three to eight seconds to evaluate a response. Three seconds is fast enough that the user waits comfortably. Eight seconds is long enough that the user wonders if the tool is broken. The loading state isn’t a spinner. It’s a designed experience that communicates “your response is being evaluated across eight dimensions by an AI judge” — specific enough that the user understands the delay and trusts the process.

The result state — a radar chart visualization showing scores across all eight dimensions, a maturity band classification from L1 to L5, and actionable guidance calibrated to the user’s specific score profile. This is the state that delivers the value proposition.

The error state — not a generic “something went wrong.” A specific message that distinguishes between “your input couldn’t be processed” and “our evaluation system encountered an issue.” Different causes, different messages, different recovery paths.

The rate-limited state — and this is the one that separates intentional architecture from afterthought engineering. Rate limiting is not an error. It’s the system protecting itself. A user who hits the rate limit isn’t doing anything wrong — they’re using the tool enthusiastically, and the system is managing its resources. The rate-limited state communicates exactly what happened, when the user can try again, and why the limit exists. It’s a designed experience, not a 429 response code.

Rate limiting is not an error. It's the system protecting itself. The user deserves to know that's what's happening.


Every one of those five states was a decision. And every decision named what was given up.

The loading state could have been a simple spinner — less development time, less design effort. I gave up simplicity because an eight-second spinner with no context creates anxiety, and anxious users close tabs. The rate-limited state could have been lumped into the error state — fewer states to manage, simpler code. I gave up simplicity because treating rate limiting as an error punishes the user for something the system chose to do.

Scene: Blueprint showing five distinct user experience states with explicit tradeoff annotations

The task dispatch system that powers the autonomous agent platform went through the same process — five alternatives rejected, each with documented rationale. Email pipeline, knowledge-base queue, scheduled batch runner, chat relay, agent sub-processes. All viable. All rejected for specific reasons: email lacked structured metadata, the queue added infrastructure dependency, the batch runner introduced latency, the chat relay couldn’t guarantee ordering, the sub-processes couldn’t survive host restarts.

The chosen architecture — a webhook-based dispatch system with persistent state — had its own tradeoffs. More complex than email. More infrastructure than a simple queue. But it satisfied the constraints that the rejected alternatives couldn’t: structured input, persistent state, ordering guarantees, crash recovery.


Architectural intentionality isn’t about making the right decision. It’s about making a decision you can explain, defend, and reverse.

The safety layer I disabled might need to be re-enabled. The five-service decomposition might need to be reconsolidated if the team shrinks. The local build scripts might need to be replaced with CI/CD if a second developer joins. The five UX states might need a sixth if a new failure mode emerges.

Every one of those reversals is possible because the original decision was documented with its rationale, its tradeoffs, and its reversal triggers. The decision record isn’t paperwork. It’s the mechanism that prevents architectural drift — the slow accumulation of decisions nobody remembers making, optimizing for constraints nobody remembers having.

Most systems don’t fail because someone made a bad decision. They fail because someone made an undocumented decision, and the person who inherited the system couldn’t tell the difference between “this was chosen deliberately” and “this is how it’s always been.”

Five states, not two. Because every state that isn’t designed is a state that defaults — and defaults don’t have rationale, tradeoffs, or reversal triggers. Defaults just accumulate until someone wonders why the system is the way it is and nobody can answer.


The Comprehension Audit evaluates architectural intentionality as one of eight scoring dimensions — assessing whether you understand not just what your AI system does, but why it’s built the way it is and what you gave up to build it that way. Try it at wilfredmorgan.com/comprehension-audit.

Wilfred Morgan

AI Systems Architect · Agentic AI Implementation

Book a Strategy Call →