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
confirmtool runs only after the executor'sApprovalPortapproves. 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 withclient_approval=Truemakes 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
LoggingHookredacts secret-looking keys and never logsconfirmtool arguments. Foreign exceptions reach the model astool-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_planwaves, portableFlowgraphs 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.