An Oliphant Never Forgets :: v1.0.0
Last login: Wed Apr 08 21:59:55 2026
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]

I was listening to a podcast (I think it was the Latent Space episode "Retrieval after RAG") and it got me thinking about JSONL files. I have all these JSONL files laying around from various projects that I'm not using, particularly from Claude Code usage. I've wanted to build event-driven agents for a while but kept struggling with what events to hook into. Somewhere in that thread of thought, I realized the architecture pattern I learned at my first engineering job maps onto agentic applications in a clean way.

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 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 was a natural fit to the Java applications written with the Spring framework that we were creating at the time. 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.

Aside

I never once swapped out an adapter at that job. The theoretical swappability was future-proofing that may not have been worth the ceremony. The real win was testability: all those interfaces and Spring's dependency injection made mocking trivial. That turned out to matter more than the architecture diagram suggested it would.

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 been 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 (not that I'm a Smalltalk expert, that was was before my time). 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

Same as classic hexagonal architecture: the central application service doesn't know anything about Kafka or HTTP (at least it shouldn't), and 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 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, you could swap out adapters without touching the domain. That was powerful on its own.

Now you can also swap out the center. Migrate from one LLM to another, update your prompts, add new guardrails; 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 where you need one.

Ports also become governance boundaries. They define what the agent is allowed to perceive and do.

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 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 everything on the backend and 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; there's no client-side state to manage.

With HTMX, the server returns HTML fragments. No component tree to keep in sync. The server decides what to render and sends it. 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 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.

What I'm Calling This

I've been calling this pattern hexagonal agents. The name borrows 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.

I plan to dig into the hard parts (latency, testing, trust) other posts. This is a fun area that I want to explore further.

visitor@garden:/notes/$