Skip to content

Agent Adapter logo Agent Adapter logo

Agent Adapter

Write a tool once, as a plain Python function.
Use it with any model API or agent framework, or from plain code.

Attach anywhere Quickstart See every feature

Which feature do I need? · Cookbook · Troubleshooting · Support matrix

What it is

Agent Adapter is an open-source (Apache-2.0), framework-independent Python SDK (version 1.1.0). It turns your functions into LLM tools, checks their schemas once at startup, and runs every call through one executor that applies approval, budgets, guards, caching and cost tracking. The core depends only on pydantic; every framework is an optional extra.

No agent framework is needed: a FastAPI service calling an LLM gets tools, approvals, budgets, cost, cache and context compression directly. The adapter can also be your framework: run_tool_loop with your own model is the loop.

flowchart LR
  F["your function<br/>@tool · wrap()"] --> R[("Registry")]
  R --> X["Executor<br/>approval · budgets · guards<br/>cache · ledger · events"]
  X --> P["plain code<br/>run · run_many · run_sync"]
  X --> W["web apps · workers<br/>FastAPI · Flask · Django · Celery"]
  X --> M["model APIs<br/>OpenAI · Gemini · Anthropic"]
  X --> A["18 framework integrations<br/>LangGraph · ADK · CrewAI · AG2 · ..."]
  X --> L["other languages and agents<br/>MCP · HTTP server · TS client · A2A · AG-UI"]

Attach anywhere

The same tool, the same executor and the same safety rules, wherever the call comes from. Attach anywhere picks the entry point for your situation.

You have Use Page
a script, a service, a notebook executor.run(...); run_sync(...) from sync code Sync code and backends
code you cannot edit registry.wrap(obj.method) Wrap a legacy class
a raw model API chat_tools, function_declarations, anthropic_tools, then executor.run_call per tool call; or run_tool_loop OpenAI, Tool loop
an agent framework one module per framework, one extra each Integrations
another language MCP, the HTTP tool server, the TypeScript client Cross-language

Safe by default

  • Approval that cannot be bypassed


    A confirm tool runs only after the executor's ApprovalPort approves. Each decision is used once, for one call: same run, call id, tool and argument digest. A framework's own "approved" signal never counts alone; a bridge passes only a one-use token the adapter issued. No port: denied. One opt-in exception: an MCP server built with client_approval=True makes the MCP client's user the approver instead of the port. The one-use rule · MCP approval

  • A locked-down HTTP server


    No authenticator is a startup error unless you opt in to anonymous mode. Run, thread and call ids are scoped to the caller; the first principal owns a run; approvals are visible to the invoker or an approver only; hooks fail closed. Security model

  • Budgets, guards and limits


    BudgetHook(max_usd), once_per_run, RunLimits, per-kind caps, rate limits, timeouts and retries, on every call from every surface. Budgets

  • No secrets in logs or errors


    LoggingHook redacts secret-looking keys and never logs confirm tool arguments. Foreign exceptions reach the model as tool-failed, without their text. Errors

Features

  • One definition — schema from the signature and docstring; portable-schema check at startup. Tools
  • Parallel execution and flows — run_many, run_plan waves, portable Flow graphs that compile to LangGraph and ADK. Flows
  • Cost and cache — a priced ledger per call; tool-result, LLM-response and provider prefix caches. Cost
  • Model ports — OpenAI, Gemini, Anthropic, your own function, with fallback and streaming. Model ports
  • Context compression — shrink long histories before each model call, in any framework's own message type. Context compression
  • Tracing — an event catalogue, sinks and OpenTelemetry GenAI spans. Tracing
  • Optimiser — an offline loop that proposes a reviewable prompt diff. Optimiser

Install

Install from PyPI (Python 3.11+); details in Install.

Command Adds
pip install -e . the core (pydantic only): tools, executor, flows, approval, cost, cache
pip install -e ".[openai]" (one extra per provider, framework or protocol) that SDK; every extra
pip install -e ".[all]" every extra that co-installs; CrewAI, Strands, Semantic Kernel, ADK, CAMEL-AI and DSPy need their own environment (why)

Quickstart

Define a tool, or wrap code you already have:

registry = Registry()


@registry.tool(kind="search", parallel_safe=True, timeout=10)
def find_article(article: str) -> str:
    """Return the text of a help-center article.

    Args:
        article: Article id, e.g. HC-101.
    """
    return f"Article {article}: how to fix it"


def delete_notes(user_id: str) -> int:
    """Delete a user's saved notes; returns how many were deleted."""
    return 3


registry.wrap(delete_notes, confirm=True, effects="destructive")  # existing code, unchanged

Build one executor with your policies, then run:

executor = Executor(
    registry,
    hooks=[LoggingHook()],  # one redacted log line per call, once logging is configured
    approval=AutoDeny(reason="no reviewer in this script"),  # confirm tools fail closed
    limits=RunLimits(max_calls=20),  # at most 20 calls per run
)
print(await executor.run("find_article", {"article": "HC-101"}))
calls = [ToolCall(name="find_article", arguments={"article": a}) for a in ("HC-102", "HC-201")]
print([r.value for r in await executor.run_many(calls)])
try:
    await executor.run("delete_notes", {"user_id": "u-1"})
except ApprovalDenied as denied:
    print("denied:", denied.reason)

The same spec renders for every model API, with nothing redefined:

spec = registry.get("find_article")
print(json.dumps(chat_tool(spec)["function"]["parameters"]["required"]))
print(json.dumps(sorted(anthropic_tool(spec))))

This code is examples/howto_attach.py; the test suite runs it. Next: the Quickstart, then how it works and the API reference.