Skip to main content

Ports and Adapters for Agents: Why Your Tool Schemas Should Outlive Your Provider

· 9 min read
Tian Pan
Software Engineer

Here is a migration story that repeats in every team shipping agents. You built your agent on one provider's SDK. Tool definitions live as JSON schemas in the exact shape that provider expects. Tool results get formatted into that provider's message structure. Then something forces a change — a better model ships from a competitor, procurement mandates a second provider for redundancy, an MCP server replaces a hand-rolled integration — and you discover the real inventory of the migration: it isn't one API client. It's every tool definition, every result formatter, every retry handler, and every test fixture in the codebase.

The failure isn't that you chose the wrong provider. It's that you let someone else's serialization format become your internal architecture. There is a twenty-year-old answer to exactly this problem — Alistair Cockburn's hexagonal architecture, better known as ports and adapters — and agent systems are the most compelling new use case it has had in years.

The tool layer is where lock-in actually lives

When engineers worry about vendor lock-in with LLMs, they usually think about prompts. Prompts do drift toward a model's idioms, but they're one string — annoying to retune, cheap to isolate. The tool layer is different in kind. A production agent might expose dozens of tools, and each one touches provider-specific format in at least four places:

  • Schema declaration — how the tool's name, description, and parameters are serialized into the request.
  • Call parsing — how the model's decision to invoke a tool comes back (a dedicated message role, a content block, a streamed delta).
  • Result formatting — how you hand the tool's output back to the model on the next turn.
  • Error and retry semantics — what happens when the model produces malformed arguments or the tool fails.

These formats genuinely diverge, and not just cosmetically. OpenAI returns tool results through a dedicated tool role; Anthropic maps them into user messages as structured content blocks. Anthropic requires system prompts as a top-level field while OpenAI accepts them in the messages array. Google's API rejects JSON schemas with untyped array items that are perfectly valid elsewhere, and no major provider accepts a top-level $ref.

The quirks go deeper than serialization. Anthropic errors if you force tool use while extended thinking is enabled. And streaming is worse than any of it: one provider emits structured multi-stage events, another emits completion chunks, and translating between them requires maintaining state across the stream.

None of these differences is hard to handle once. The problem is where teams handle them. When the provider's format is the format your tools are written in, every quirk is handled at every tool definition, and the count of places you must touch during a migration scales with the number of tools — not with the number of providers.

Ports: the schema you own

Hexagonal architecture's core move is to draw the dependency arrow inward. The application core defines ports — stable interfaces expressed in the domain's own vocabulary — and the messy outside world connects through adapters that implement those ports. The core never imports the adapter; the adapter imports the core.

Applied to agents, the port is your tool contract: a name, a human-and-model-readable description, a parameter schema, a result type, and error semantics — written once, in a neutral representation that you version and own. Not "the OpenAI function-calling format" and not "whatever the MCP server advertises," even if your neutral format happens to look similar to one of them. The distinction isn't the syntax; it's the ownership. When you own the contract, a provider changing its serialization is an adapter bug. When the provider owns it, the same change is a codebase-wide event.

From that single contract, thin adapters do mechanical translation in both directions:

  • A provider adapter serializes your tool contracts into each vendor's request format and parses tool-call decisions back out of each vendor's response format — including the streaming variants.
  • A backend adapter connects the contract's execution side to whatever actually does the work: a local function, an internal REST service, an MCP server, a queue.

This is the same shape as an LLM gateway, and gateways are a fine way to buy the provider-adapter half off the shelf. But a gateway only normalizes the model-facing side. The backend side — the fact that "search_orders" is today a Postgres query and next quarter an MCP server owned by another team — is your domain, and no vendor product will define those contracts for you.

The boundary that matters isn't agent-versus-tools

Most agent framework diagrams draw one boundary: the agent on one side, tools on the other. That boundary is real but it's not the one that hurts. The expensive boundary is between your semantics and everyone else's serialization — and it cuts through both sides of the diagram.

Loading…
References:Let's stay in touch and Follow me for more thoughts and updates