AI/TLDR

Agent Framework Lock-In: How to Stay Portable

Understand exactly where an agent framework locks you in — and the concrete design patterns that keep your prompts, tools, and logic portable if you ever need to leave.

INTERMEDIATE11 MIN READUPDATED 2026-06-13

In plain English

When you build an AI agent, you rarely wire everything from scratch. You reach for an agent framework like LangGraph, the OpenAI Agents SDK, or the Claude Agent SDK. The framework hands you tool calling, memory, retries, and orchestration for free. That convenience has a price: your code starts to speak the framework's language.

Framework Lock-In & Portability — illustration
Framework Lock-In & Portability — images.prismic.io

Lock-in is what happens when so much of your application is written in that language that leaving becomes painful. It is not a single switch someone flips. It is a slow accumulation: a tool defined the framework's way here, a memory object passed around there, a callback hooked into its event system. Each one is small. Together they become a wall.

Think of renting a furnished apartment. Moving in is effortless — the sofa, the bed, the kitchen are all there. But the longer you stay, the more your things get tangled with theirs. Move-out day, you discover the shelves you filled are bolted to their walls. The question this article answers is: how do you live comfortably in the furnished apartment while keeping your own belongings in boxes you can actually carry out?

Why it matters

Agent frameworks are young and the field moves fast. The library you pick today may be deprecated, acquired, repriced, or simply out-paced in eighteen months. You may also outgrow it: a framework that was perfect for a prototype can become a straitjacket once you need fine control over latency, streaming, or cost. Portability is insurance against a future you cannot predict.

Concrete reasons a team ends up wanting to switch:

  • The framework changes under you. A major version rewrites the API, or a feature you depend on is dropped. Now an upgrade is itself a migration.
  • You hit a ceiling. You need custom retry logic, a streaming behavior, or a control-flow shape the framework's abstractions fight against. Working around the framework costs more than the framework saved.
  • Cost or vendor pressure. A provider-specific SDK ties your orchestration to one model vendor. When pricing or model quality shifts, you want to move the underlying model without rewriting the agent.
  • Team or stack consolidation. The company standardizes on a different framework, or you inherit a second agent built on another one and want them to share infrastructure.

Here is the trap most teams fall into: lock-in is invisible while everything is going well. You only feel the wall on the day you try to leave — which is the worst possible day to discover how high it is. The work in this article is cheap to do up front and brutally expensive to retrofit. That asymmetry is the whole reason to care now rather than later.

How lock-in actually forms

An agent is not one thing; it is a stack of layers. Lock-in is uneven across them — some layers are trivially portable, others bind you tightly. Knowing which is which is the whole game. Here is the typical stack, from the parts that are yours down to the parts the framework owns.

The pattern is clear: *the closer a layer is to meaning, the more portable it is; the closer it is to plumbing, the stickier it gets.* Your prompts are just strings — they move anywhere. Your orchestration graph is expressed entirely in the framework's API — it moves nowhere without a rewrite. Most of your design effort should go into pushing valuable logic up the stack, away from the framework's plumbing.

Where the coupling hides

LayerWhat ties you inPortability
PromptsNothing — they are plain text you authorHigh
Tool functionsThe body is yours; the decorator/schema that registers it is framework-specificHigh (body) / Low (registration)
Control flowWhether your logic lives in your code or in their graph nodes/handoffsDepends on you
Memory / stateFramework State or Context objects passed between stepsLow
OrchestrationThe graph, agent-loop, or runner — the framework's reason to existVery low
ObservabilityCallback/tracing hooks bound to their lifecycle eventsVery low

Notice the tool row. The logic of a tool — call an API, query a database, do a calculation — is pure Python or TypeScript that owes nothing to any framework. What binds you is the thin wrapper that describes that tool to the framework: the decorator, the schema format, the way arguments are validated. That distinction is the key to the main escape pattern, below.

The thin-wrapper pattern

The single most effective portability technique is also the oldest idea in software: put a seam between your code and their code. Define your own small interface for what an agent needs to do, and let the framework live behind it. Your application talks to your interface; only one thin adapter talks to the framework. When you switch frameworks, you rewrite the adapter — not the application.

Before: framework calls everywhere

In the typical first draft, framework imports and types are smeared across the whole codebase. Every module knows which framework you use.

before — coupled directly to the frameworkpython
from some_framework import Agent, tool, Runner

@tool  # framework decorator wraps your logic
def lookup_order(order_id: str) -> str:
    return db.get_order(order_id)

# Orchestration expressed in the framework's primitives
agent = Agent(
    name="support",
    instructions=SUPPORT_PROMPT,
    tools=[lookup_order],
)

def handle(message: str) -> str:
    result = Runner.run_sync(agent, message)   # framework type leaks out
    return result.final_output                  # framework-shaped result

The problem: @tool, Agent, Runner, and result.final_output all belong to the framework, and they appear in code your whole app calls. To switch, you touch every one of these sites.

After: one adapter, plain logic everywhere else

Now the tool logic is a plain function with no decorator. Your app depends on an AgentClient interface you defined. A single adapter file translates that interface into framework calls — and is the only place that imports the framework.

after — your interface, framework behind one adapterpython
# tools.py — plain functions, zero framework imports
def lookup_order(order_id: str) -> str:
    return db.get_order(order_id)

TOOLS = {
    "lookup_order": {
        "fn": lookup_order,
        "description": "Look up an order by its ID.",
        "params": {"order_id": "str"},
    },
}

# port.py — YOUR interface; the rest of the app depends only on this
class AgentClient(Protocol):
    def run(self, message: str) -> str: ...

# adapter_framework.py — the ONLY file that imports the framework
from some_framework import Agent, tool, Runner

class FrameworkAgent:
    def __init__(self, prompt: str, tools: dict):
        wrapped = [tool(t["fn"]) for t in tools.values()]  # wrap here, once
        self._agent = Agent(instructions=prompt, tools=wrapped)

    def run(self, message: str) -> str:
        return Runner.run_sync(self._agent, message).final_output

Keep the interface small. The temptation is to expose every framework feature through your port; resist it. The narrower your interface, the cheaper every adapter. If you expose run(message) -> str plus a streaming variant, almost any framework can satisfy it. If you expose the framework's full graph API, you have rebuilt the lock-in inside your own wrapper.

A portability checklist

Run through this before you commit to a framework, and again every few months as the codebase grows. Each item is a place lock-in quietly accretes.

  1. Are prompts stored as plain data? Keep system prompts and instructions in your own files or config, not inlined inside framework objects. They are your most valuable, most portable asset.
  2. *Is tool logic* free of framework decorators?** The function should run standalone in a unit test with no framework imported. Registration lives in the adapter only.
  3. *Does your app depend on a your* interface, not framework types?** If a framework class name appears outside the adapter, that is a leak. Search your imports.
  4. Is state expressed in your own types? Convert framework State/Context objects to plain dicts or dataclasses at the boundary so memory does not bind you.
  5. Is the model choice separable from the orchestration? You want to swap the underlying model (or even provider) without touching agent logic.
  6. Is observability behind an interface? Emit your own structured events; let the adapter forward them to the framework's tracing, not the reverse.
  7. Do you have an evaluation set that is framework-agnostic? A suite of input/expected-behavior cases lets you prove a new framework matches the old one's quality before you cut over.

When lock-in is fine to accept

Portability is not free. The thin-wrapper pattern adds an interface, an adapter, and a layer of indirection. Sometimes that cost is not worth paying, and pretending otherwise is just gold-plating. Be honest about which situation you are in.

A useful middle path: do the cheap portability items always, skip the expensive ones until you need them. Keeping prompts as data and tool logic decorator-free costs almost nothing and pays off immediately in testability. Building a full adapter layer for orchestration is real work — defer it until the system is clearly long-lived. You can always extract a seam later if the early items kept your logic clean. The reverse — untangling framework calls from a sprawling codebase — is the migration you were trying to avoid.

If you are still choosing a framework rather than escaping one, the companion guides cover that decision directly: how to choose an agent framework, the framework comparison, and the mental models that frame the trade-offs.

Going deeper

Once the thin-wrapper basics click, a few subtler points separate teams that stay genuinely portable from those that only think they are.

Leaky abstractions are the silent killer. An interface that returns a plain string is portable; one that returns the framework's rich result object — with its token counts, tool-call traces, and step history — has smuggled the framework back across your boundary. The day you switch, every caller that read .tool_calls breaks. Audit your interface for framework types in signatures, not just framework imports. If your run() returns FrameworkResult, the seam is decorative.

Standards reduce, but do not erase, lock-in. Open conventions help. The Model Context Protocol standardizes how tools and data sources plug into agents, so a tool you expose over MCP is reusable across any MCP-aware framework — a genuine portability win for the tool layer. But MCP does not standardize orchestration, memory, or control flow, so the stickiest layers stay sticky. Treat standards as lowering specific walls, not as a portability silver bullet.

Provider SDKs trade portability for power. A vendor's own agent SDK is often the best, fastest way to use that vendor's models — but it couples orchestration to one provider on purpose. That is a legitimate choice; just make it knowingly. Put the thin wrapper around it exactly as you would any framework, so that if you later want a multi-provider setup, the model swap is an adapter change, not an app rewrite.

Test at the seam. The interface you defined is the perfect test boundary. Write your behavior tests against AgentClient, with a fake adapter that returns canned responses, so your business logic is tested without any LLM call at all. Then run the same evaluation suite against each real adapter. This is what makes a framework migration a verified change rather than a hopeful one — you prove the new adapter passes the exact tests the old one did.

The durable lesson: lock-in is a property of where your valuable logic lives, not of which framework you picked. Keep meaning — prompts, tool logic, control decisions, evaluation cases — in code you own, and keep plumbing behind one thin adapter. Do that, and the question "which framework?" stops being a one-way door and becomes what it should be: a reversible, low-stakes choice you can revisit whenever the landscape shifts.

FAQ

What is agent framework lock-in?

It is the accumulated cost of having your agent code written in one framework's specific abstractions — its tool decorators, state objects, graph API, and callbacks — so that switching to a different framework requires rewriting large parts of your application rather than swapping one component.

How do I avoid vendor lock-in with AI agents?

Define your own small interface for what your agent must do (for example run(message) -> str), keep prompts as plain data and tool logic free of framework decorators, and let a single thin adapter translate your interface into framework calls. Then switching frameworks means rewriting one adapter, not your whole app.

Is it worth making my agent framework-agnostic?

It depends on the stakes. For a long-lived production system, multiple agents, or a volatile/young framework, yes — the cheap portability steps cost almost nothing and the seam pays off if you ever migrate. For a short-lived prototype or a single small agent, using the framework directly is usually the right call.

Which parts of an agent are hardest to make portable?

Orchestration (the graph or agent-loop) and observability (callbacks tied to the framework's lifecycle) are the stickiest, because they are expressed entirely in the framework's core API. Prompts and tool logic are the easiest to keep portable — prompts are just text, and a tool's body is plain code that only its registration wrapper ties to the framework.

Does using Model Context Protocol remove framework lock-in?

It reduces it for the tool layer. MCP standardizes how tools and data sources connect to agents, so an MCP tool is reusable across MCP-aware frameworks. But MCP does not standardize orchestration, memory, or control flow, so those layers stay framework-specific. It lowers specific walls rather than removing lock-in entirely.

How much does switching agent frameworks actually cost?

It depends almost entirely on how coupled your code is. With framework calls smeared across every module, a switch touches every file and is effectively a rewrite. With a thin-wrapper seam and a framework-agnostic evaluation set, it is rewriting one adapter and re-running your tests — a contained, verifiable change.

Further reading