Skip to content

Quickstart

Start with one function and one call. Then define two tools, render them for two providers, and run them through an executor of your own. Five minutes.

0. Hello world

This is examples/hello.py. The test suite runs it.

"""Hello world: one function, one call, through the default executor."""

import agent_adapter as aa


@aa.tool
def add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


print(aa.call(add, a=1, b=2))  # 3: validated and guarded by the default executor
Line Effect
@aa.tool registers add on the default registry; the function is unchanged
aa.call(add, a=1, b=2) runs it through the default executor: validation, hooks, approval, guards; prints 3

aa.call is for sync code; await aa.acall(add, a=1, b=2) is its async twin. Both use the default executor. Set its options once with aa.configure(...), which takes the Executor arguments and returns the executor; reset_default_executor() (from agent_adapter.exec) drops it, for example between tests. A function that is not registered is refused (ToolUsageError); it never runs unchecked.

1. The whole program

This is examples/quickstart.py. The test suite runs it, so it stays correct.

"""Quickstart: define tools once, render them for two providers, run them with the executor."""

import asyncio
import json

from agent_adapter import AutoDeny, Executor, Registry, ToolCall, ToolContext
from agent_adapter.integrations.gemini import function_declarations
from agent_adapter.integrations.openai import chat_tools

registry = Registry()


@registry.tool(kind="retrieval", parallel_safe=True)
async def lookup_article(article: str, lang: str = "en", ctx: ToolContext | None = None) -> str:
    """Return the text of a help-center article.

    Args:
        article: Article topic, e.g. returns.
        lang: Language code.
    """
    return f"Article {article} ({lang})"


def legacy_rate(item: str, region: str) -> float:
    """Return the shipping rate for an item, in percent of the order value."""
    return {"eu": 5.0, "us": 18.0}.get(region, 12.0)


registry.wrap(legacy_rate, name="shipping_rate", fixed={"region": "us"})

# One executor per process: approval, budgets, guards, cache and ledger live here.
executor = Executor(registry, approval=AutoDeny())


async def main() -> None:
    print(json.dumps(chat_tools(registry), indent=2))
    print(json.dumps(function_declarations(registry), indent=2))
    print(await executor.run("lookup_article", {"article": "returns"}))
    # A tool call from a model: name, parsed arguments and the provider's call id.
    call = ToolCall(name="shipping_rate", arguments={"item": "laptop"}, id="call_1")
    result = await executor.run_call(call)  # a ToolResult; failures are in result.error
    print(result.value if result.ok else result.error)


if __name__ == "__main__":
    asyncio.run(main())

2. What each part does

flowchart LR
  A["@registry.tool<br/>lookup_article"] --> R[("registry")]
  B["registry.wrap(legacy_rate,<br/>fixed={'region': 'us'})"] --> R
  R --> O["chat_tools()"]
  R --> G["function_declarations()"]
  R --> X["executor.run(name, args)<br/>executor.run_call(call)"]
Line Effect
Registry() an empty set of tools
@registry.tool(kind="retrieval", parallel_safe=True) builds the schema from the signature and docstring; the function is unchanged
ctx: ToolContext | None = None injected at call time; never in the schema
registry.wrap(legacy_rate, name="shipping_rate", fixed={"region": "us"}) exposes existing code; region is bound and hidden
chat_tools(registry) OpenAI Chat Completions tool dicts (strict)
function_declarations(registry) Gemini function declaration dicts
Executor(registry, approval=AutoDeny()) one per process; holds approval, budgets, guards, cache and the ledger. No reviewer here, so a confirm tool is denied
executor.run("lookup_article", {"article": "returns"}) validates the arguments, applies the policies, calls the function, returns the value or raises
executor.run_call(ToolCall(...)) the same for a model's tool call; returns a ToolResult, with failures in result.error

Renderers are imported by name from agent_adapter.integrations.<provider>, so the SDK names openai and google stay free.

The executor acts on kind and parallel_safe (per-kind caps, the run lock).

3. Run it

python examples/quickstart.py

You get the OpenAI tools as JSON, the Gemini declarations as JSON, then:

Article returns (en)
18.0

lang was not sent, so its default en applied. region came from fixed=.

4. Send it to a real model

Pass the rendered list as tools= to your SDK client. Run each tool call the model sends through the one executor, never with spec.invoke:

# A tool call from a model: name, parsed arguments and the provider's call id.
call = ToolCall(name="shipping_rate", arguments={"item": "laptop"}, id="call_1")
result = await executor.run_call(call)  # a ToolResult; failures are in result.error
print(result.value if result.ok else result.error)

spec.invoke only validates and calls. It skips approval, budgets, run guards, the cache and the ledger, so a confirm=True tool would run unapproved. Keep it for tests. To let the adapter drive the whole model loop, use run_tool_loop.

Each provider page shows the exact shapes:

Or start from a template

agent-adapter init writes a tested starter file and a requirements.txt:

agent-adapter init pydantic-ai my-agent   # also: adk, agno, strands, crewai
cd my-agent
pip install -r requirements.txt
python agent.py                           # crewai writes crew.py: python crew.py
flowchart LR
  T["agent.py"] --> R[("Registry: shipping_rate, refund_order")]
  R --> B["framework tools<br/>(the integration bridge)"]
  B --> E["Executor"]
  E --> A["approval port: review()"]
  E --> G["BudgetHook: 1 USD"]
  E --> V["recorder: .agent_adapter/runs"]

Each framework template has two tools, one with confirm=True, an approval port, a budget and the run recorder. The budget counts model calls made through executor.model(...), not the framework's own model calls. Without AGENT_MODEL it runs offline: it prints the tools and makes one approved call. Set AGENT_MODEL (the framework's own model name) to run the agent. Then run agent-adapter ui to see the run. Every template: CLI.

Next