An Oliphant Never Forgets :: v1.0.0
Last login: Welcome
System: oliphant/fastapi
Type help for commands
$ cat ~/notes/.md

Hexagonal Agents: What If Your App's Business Logic Was an AI Agent?

notes 2026-03-18 [evergreen]

What if we replaced the deterministic business logic at the center of a web application with a non-deterministic AI agent? And what if we paired it with a frontend framework that lets the server generate HTML on the fly, so the UI could be just as flexible as the agent behind it?

I've been experimenting with this idea, and it works better than I expected. Here's the architecture I've landed on, and why I think it points toward something real.

The Old Pattern

When I started out in software engineering at Nordstrom in 2015, I was on a team migrating from a monolith to microservices. A principal engineer on my team was obsessed with hexagonal architecture, and I ended up using it constantly when building out services. The basic idea:

  • A central domain layer containing business logic (think Domain-driven Design)
  • Ports: interfaces for interacting with the outside world
  • Adapters: concrete implementations of those interfaces

This mapped cleanly to Java applications written with the Spring framework. The beauty was isolation. The core domain didn't know or care whether it was talking to Kafka, a REST controller, or a CLI. You could swap out SQLite for Postgres by writing a new adapter, and the rest of the application stayed the same.

The New Pattern

Over the past year, as I've been writing less and less code myself (something lots of people have talked about), I've found myself revisiting architecture patterns at a higher level and asking how they apply to agentic applications.

The key insight: the center of the hexagon doesn't have to be a rules engine. It can be a reasoning engine.

  • The AI agent core replaces deterministic business logic with prompts, guardrails, validation policies, and tool permissions
  • Inbound ports become agent triggers
  • Outbound ports become agent capabilities

There's a useful framing for thinking about these agent cores. Scott Werner describes prompt objects, borrowing from Alan Kay's Smalltalk: everything is an object, objects communicate by sending messages, and the receiver interprets the message at runtime. LLMs turn out to be the "universal interpreter" that Smalltalk always assumed but never had. This gives us semantic late binding, where meaning resolves at runtime via natural language rather than at compile time via function signatures. A prompt object doesn't need to know what methods another object has. It sends a message and trusts the receiver to figure it out. That maps directly onto the hexagonal structure: ports are message channels, and the agent core is the interpreter.

Inbound Side

Inbound ports define how the world talks to your agent:

  • handle_user_message(input)
  • handle_event(event)
  • execute_task(task_id)
  • process_email(email)

Inbound adapters are the concrete entry points:

  • REST controller
  • WebSocket handler
  • Kafka consumer
  • CLI interface
  • Slack bot webhook

Just like how classic hexagonal architecture meant that the central application service didn't know anything about Kafka or HTTP (at least it shouldn't), the AI agent doesn't need to know the specific technologies implementing those interfaces.

Outbound Side

In classic hexagonal terms, you might have a UserRepository port with a PostgresUserRepository adapter. With AI agents, this gets reframed as agent capabilities. The agent doesn't call specific technologies. It invokes capabilities:

class EmailSenderPort:
    def send_email(to, subject, body)

class SearchPort:
    def search(query)

class DatabasePort:
    def store_memory(data)

Outbound adapters could be:

  • MCP servers
  • Tool calling interfaces
  • REST clients
  • Message buses
  • Database clients

One thing I've learned building with MCP tools: every tool you expose to an agent adds permanent context-window load, and each tool call is a full LLM reasoning cycle. It's tempting to create a port for every capability you could expose, but the better approach is to design ports around workflows the agent actually needs to complete. A search_and_summarize tool is more useful than separate fetch_url, extract_text, and call_llm tools. This is the same discipline hexagonal architecture always demanded (don't create interfaces you don't need), but it matters more here because the cost of a bloated adapter surface is paid in tokens on every single request.

Why This Is More Composable

In the old hexagonal architecture, we could swap out adapters without touching the domain. That was powerful on its own.

Now we can also swap out the center. Migrate from one LLM to another, update your prompts, add new guardrails, and the ports and adapters stay the same. Run your tests and evals against the new core to check for regressions. The architecture gives you a clean boundary around the non-deterministic part, which is exactly where you need one.

Ports also become governance boundaries. They define what the agent is allowed to perceive and do. Instead of the old dependency tangle where business logic reached into infrastructure code, ports enforce capability boundaries by design.

One nuance worth calling out: not all governance should be optional. If you rely on the agent to voluntarily call a safety-check tool, it might just... not. For critical behaviors like permission checks or content validation, mandatory hooks (system-level triggers that fire regardless of what the agent decides to do) are more reliable than tools the agent can choose to ignore. In hexagonal terms, some governance lives at the port layer as a hard constraint, not in the agent core as a soft guideline.

Why HTMX Fits

In October 2023, I came across the HOWL stack (Hypertext On Whatever Language you like). At the time, I had zero frontend experience and was overwhelmed by the JavaScript framework landscape. HTMX offered a different path: do almost everything on the backend and just send HTML to the browser.

I tinkered with it and kept it in the back of my head. It turns out HTMX is a natural fit for agent-driven backends, for a specific reason: the server already controls the UI.

With HTMX, the server returns HTML fragments. There's no client-side state to manage, no component tree to keep in sync. The server decides what to render and sends it. This means an agent on the backend can generate different HTML depending on what the user asked for, without needing a rigid frontend contract.

There's a useful design boundary here: keep the page shell (layout, navigation, styles) as a fixed template, and let the agent generate only the content fragments that swap into it. HTMX partials should slot into an existing page structure, not re-render the whole page. This gives you a clean separation between what the framework owns (structure, styling, routing) and what the agent owns (content, data presentation, interactive elements). The shell is your port boundary on the frontend side.

Want your data filtered a particular way? The agent just returns it that way. Want a different view of the same data? The agent generates the appropriate HTML fragment. You don't need to anticipate every feature up front, because the agent can compose new responses on the fly.

The Hard Parts

I don't want to oversell this. A non-deterministic UI introduces real problems:

  • Latency: LLM calls are slow compared to template rendering. One mitigation is letting users save views they like, so the agent only generates novel layouts when asked.
  • Cost: Every UI render costs tokens. Caching common responses helps, but you need to think carefully about cache invalidation when the underlying data changes.
  • Testing: How do you write assertions against output that varies on every run? This probably needs a combination of structural tests (did the response contain valid HTML with the right data?) and eval-style checks.
  • Trust: Users expect consistency. If the same page looks different every time, that's disorienting. There's a design challenge in balancing flexibility with predictability.

I plan to dig into these problems in a follow-up post.

What I'm Calling This

I've been calling this pattern hexagonal agents. The name borrows directly from hexagonal architecture because the structural insight is the same: isolate your core from the outside world through well-defined interfaces. The difference is that the core is now a reasoning engine rather than a rules engine, and the frontend is fluid rather than fixed.

visitor@garden:/notes/$
Processing