ag2-add-custom-tool
Skill12 starsChanged 10 days agoApache-2.0View on GitHub
Add a custom Python tool to an AG2 `Agent` using the `@tool` decorator. Use when the user wants to give an Agent a new capability backed by Python code (API calls, DB queries, computations, file ops) or needs tools to share state. Covers sync and async tools, `@tool(timeout=...)`, parameter typing, Pydantic `Field` schema customisation, returning typed `Input` / `ToolResult` (text / data / images / binary), `final=True` early-exit, dependency injection via `Context` / `Inject` / `Variable` / `Depends`, and conversation variables (`ask(variables=...)`, reading results back from `reply.context.variables`, `ScopedValue` / `Scope` / `ScopeError` for subagents). For built-in tools see `ag2-use-builtin-tools`; delegation `ag2-subagent-delegation`; approval gates `ag2-hitl`; testing `ag2-testing`.
1 other project uses a version of this, 1 of them edited.
Adopted by
Most recently taken up by
- ag2ai/build-with-ag2edited, first seen Oct 9, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 1
- ag2ai/build-with-ag2★ 25218% same
Wrote their own version · 0
None found.
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository27 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- ag2-a2aSkill
- ag2-acpSkill
- ag2-ag-uiSkill
- ag2-eval-comparisonSkill
- ag2-evaluationSkill
- ag2-hitlSkill
- ag2-knowledge-and-memorySkill
- ag2-liveSkill
- ag2-mcpSkill
- ag2-middlewareSkill
- ag2-multimodal-inputSkill
- ag2-network-discussionSkill
- ag2-network-governanceSkill
- ag2-network-quickstartSkill
- ag2-network-tools-and-viewsSkill
- ag2-network-workflowSkill
- ag2-observers-and-alertsSkill
- ag2-overviewSkill
- ag2-quickstartSkill
- ag2-shell-toolSkill
- ag2-structured-outputSkill
- ag2-subagent-delegationSkill
- ag2-telemetrySkill
- ag2-testingSkill
- ag2-use-builtin-toolsSkill
The file
Files
Add a custom Python tool
When to use
The user wants their Agent to take a real-world action: hit an API, query a database, compute something, return an image — or tools must share state across calls. For shipped tools (web search, code exec, shell) see ag2-use-builtin-tools / ag2-shell-tool.
60-second recipe
from ag2 import Agent, tool
from ag2.config import OpenAIResponsesConfig
@tool
def calculate_shipping_cost(destination: str, weight_kg: float) -> str:
"""Calculates shipping cost for a package to a destination."""
return "$15.00"
agent = Agent(
"shipping",
prompt="Use tools when helpful.",
config=OpenAIResponsesConfig(model="gpt-4.1-mini"),
tools=[calculate_shipping_cost],
)
reply = await agent.ask("Cost to ship 2kg to Berlin?")
print(reply.body)@tool builds the LLM-facing schema from the signature, type hints and docstring. The docstring is the description the LLM sees. Plain undecorated functions in Agent(tools=[...]) are wrapped automatically, but not in per-call agent.ask(..., tools=[...]) — there a plain function raises AttributeError in 1.3.0, so decorate it with @tool first; @agent.tool attaches to an existing agent. Tool names must be unique per turn — a later tool with the same name replaces the earlier one (with a warning).
Sync, async, timeouts
Sync def tools run in a worker thread by default (@tool(sync_to_thread=False) only for cheap pure-Python work). async def tools run on the event loop.
@tool(timeout=30.0) # seconds; default None = wait forever
async def fetch_page(url: str) -> str:
"""Fetches the text of a web page."""
async with httpx.AsyncClient(timeout=20.0) as client:
return (await client.get(url)).textOn expiry the model gets a TimeoutError tool result (ToolErrorEvent) and can continue. A timed-out sync tool's thread keeps running — also set timeouts on the underlying client.
Schema control with Pydantic Field
from typing import Annotated
from pydantic import Field
@tool(name="set_thermostat", description="Sets the thermostat.")
def set_temperature(
temp: Annotated[int, Field(description="Target °C.", ge=10, le=30)],
mode: Annotated[str, Field(pattern="^(heat|cool|auto)$")],
) -> str:
return f"Set to {temp}°C in {mode} mode."Returning typed Input / ToolResult
A str return becomes TextInput. For richer payloads return an Input subtype or compose with ToolResult:
from ag2 import DataInput, ImageInput, ToolResult, tool
@tool
def get_user_profile(user_id: str) -> DataInput:
"""Look up a user."""
return DataInput({"id": user_id, "name": "Alice"})
@tool
def analyze_product(product_id: str) -> ToolResult:
"""Returns image + structured metadata in one tool call."""
return ToolResult(ImageInput(url=f"https://cdn.example.com/{product_id}.jpg"), {"id": product_id, "stock": 42})Raw bytes: BinaryInput(data, media_type="application/pdf").
End the turn early: return ToolResult("Ticket 42 was escalated.", final=True) — the text becomes reply.body with no extra LLM round-trip. A final result must hold exactly one part (TextInput or DataInput).
Dependency injection & shared state
Hidden from the LLM schema; full guide in references/dependency_injection.md.
| Annotation | Resolves from | Use for |
ctx: Context | the current context | .variables, .dependencies, .input() |
Annotated[T, Inject("name")] | dependencies={...} | clients, DB pools, locks, secrets (never serialized) |
Annotated[T, Variable("name", default=...)] | variables={...} | serializable state shared between tools |
Annotated[T, Depends(fn)] | calls fn per tool call (may yield for teardown) | sessions, auth checks |
from ag2 import Context, Variable
@tool
def authenticate(context: Context) -> str:
"""Log in. Call before fetch_secure."""
context.variables["auth_token"] = "abc-123"
return "Authenticated"
@tool
def fetch_secure(auth_token: Annotated[str | None, Variable(default=None)]) -> str:
"""Fetch secure data."""
return f"Data with token {auth_token}" if auth_token else "Not authenticated"
inputs = {"user": "u1"}
reply = await agent.ask("Get my data.", variables=inputs)
reply.context.variables["auth_token"] # "abc-123" <- read results HERE
inputs # {"user": "u1"} — unchangedask(variables=...) deep-copies the mapping (merged over Agent(variables=...) defaults) — tool mutations never reach your original dict or the agent defaults. Read state back from reply.context.variables; reply.ask(...) continues with the same variables. For subagents (Agent.as_tool()), variables are copied into each child unless declared ScopedValue(value, scope=Scope.SHARED) at the root — see the reference.
Going deeper
references/dependency_injection.md—Inject/Variable/Dependsdetails,Scopesemantics, test overrides.- Upstream:
website/docs/user-guide/tools/tools.mdx,context/variables.mdx,context/inject.mdx,depends.mdx,tools/toolkits.mdx,tools/tool_middleware.mdx.
Common pitfalls
- Vague docstring / missing type hints — the LLM won't know when or how to call the tool.
- Expecting your
variablesdict to be mutated — it's copied; usereply.context.variables. - Putting clients/locks in
variables— they're deep-copied (and synced over A2A/AG-UI). Usedependencies+Inject. - Blocking the loop with
sync_to_thread=Falseon slow code — keep the default. - Swallowing
asyncio.CancelledErrorin async tools — breakstimeout=. final=Truewith multiple parts — raises.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

