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¶
You get the OpenAI tools as JSON, the Gemini declarations as JSON, then:
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¶
- Wrap existing code: methods, partials, rename, hide.
- Tools: every option.