Giving Agents Tools
Tools Work the Same Way, With Less Boilerplate
Unit 8 covered function calling in depth: defining a JSON Schema for a function's arguments, checking response.output for a function_call item, executing the actual Python function, and feeding the result back in. Every one of those underlying concepts still applies when an agent uses a tool — what changes is how much of that plumbing you have to write yourself. The Agents SDK turns an ordinary Python function into a usable tool with a single decorator, deriving the schema Unit 8, Lesson 2 taught you to write by hand directly from the function's own signature and docstring.
Defining a Tool
from agents import Agent, Runner, function_tool
@function_tool
def get_order_status(order_id: str) -> str:
"""Look up the current status of a customer's order.
Args:
order_id: The order identifier, e.g. "4471".
"""
order_statuses = {"4471": "shipped", "5502": "processing"}
return order_statuses.get(order_id, "order not found")
support_agent = Agent(
name="Support Agent",
instructions="You help customers check their order status.",
model="gpt-5.6-terra",
tools=[get_order_status],
)
result = Runner.run_sync(support_agent, "What's the status of order 4471?")
print(result.final_output)
Note: The exact decorator name, and how strictly it requires type hints and docstrings to build an accurate schema, can vary by SDK version. Confirm the current requirements against the current official documentation.
The @function_tool decorator is doing exactly the work Unit 8, Lesson 2 walked through by hand: inspecting get_order_status's parameter (order_id: str) and its docstring to build a JSON Schema tool definition equivalent to what you'd have written manually as a {"type": "function", "name": ..., "parameters": {...}} dictionary. This is the single biggest reduction in boilerplate the Agents SDK offers over the direct approach: the schema is derived from the function itself, rather than maintained as a separate, parallel description that has to be kept in sync with the function's actual signature by hand.
Why the Docstring Actually Matters Here
Unit 8, Lesson 2 emphasized writing clear, specific descriptions in a tool's schema, since the model relies entirely on that description to decide when and how to call it. With @function_tool, the docstring is that description — it's not documentation for other developers that happens to also be nice to have, it's the actual text the model sees when deciding whether this tool is relevant to a given request.
@function_tool
def issue_refund(order_id: str, amount: float) -> str:
"""Issue a refund for a specific order.
Use this only after confirming the customer is eligible for a refund
under the return policy. Do not use this for shipping cost adjustments.
Args:
order_id: The order identifier to refund.
amount: The refund amount in US dollars.
"""
return f"Refunded ${amount:.2f} for order {order_id}."
A vague docstring here — just "issues a refund" — would give the model far less to work with than the version above, which specifies exactly when the tool should and shouldn't be used, directly mirroring Unit 8, Lesson 2's guidance to write descriptions that disambiguate a tool from similar ones and specify its intended conditions of use, not just what it technically does.
Giving an Agent Several Tools
An agent can be given a list of tools, exactly as tools=[...] accepted a list of schemas in Unit 8, Lesson 4's multiple-tools pattern.
@function_tool
def get_return_policy() -> str:
"""Return the current return and refund policy text."""
return "Items may be returned within 30 days of purchase for a full refund."
support_agent = Agent(
name="Support Agent",
instructions="You help customers with orders, refunds, and policy questions.",
model="gpt-5.6-terra",
tools=[get_order_status, issue_refund, get_return_policy],
)
The model chooses among these tools using exactly the same reasoning Unit 8, Lesson 4 described for multiple custom functions — comparing the user's request against each tool's name and description to decide which, if any, are relevant — the Agents SDK doesn't change this underlying decision process, only how the tools were defined and how the resulting calls get executed.
Handling a Tool That Fails
Unit 8, Lesson 5 covered treating a function's arguments as untrusted input and curating safe error messages rather than exposing raw exception details. The same discipline applies to a tool used by an agent, since an unhandled exception inside a tool function is just as much a real risk here as it was for a hand-written function-calling loop.
@function_tool
def get_order_status_safely(order_id: str) -> str:
"""Look up the current status of a customer's order.
Args:
order_id: The order identifier, e.g. "4471".
"""
order_statuses = {"4471": "shipped", "5502": "processing"}
if not order_id.isdigit():
return "Invalid order ID format. Order IDs are numeric."
return order_statuses.get(order_id, "No order found with that ID.")
Returning a clear, safe message for an invalid or missing order — rather than letting an unhandled KeyError or similar exception propagate — follows the same principle Unit 8, Lesson 5 established: a tool's failure should be curated into something the model (and, ultimately, the user) can act on sensibly, not a raw error that leaks implementation detail or produces a confusing final answer.
Combining Custom Tools With Built-In Tools
An agent isn't limited to @function_tool-decorated custom functions; it can also be given the platform's own built-in tools from Unit 9 — web search, file search, and Code Interpreter — in the same tools list.
from agents import WebSearchTool
research_agent = Agent(
name="Research Agent",
instructions="Answer questions using web search when they require current information.",
model="gpt-5.6-terra",
tools=[WebSearchTool()],
)
Note: The exact classes and names the Agents SDK uses to expose built-in tools (web search, file search, Code Interpreter) can vary by SDK version. Confirm the current interface against the current official documentation.
This is the same combination Unit 9, Lesson 4 covered for a single client.responses.create() call — custom functions and platform built-in tools registered together, with the model choosing the right one per sub-question — except here the combination is attached once to a reusable Agent rather than assembled fresh into a tools list on every individual call.
Testing a Tool Function Independently of the Agent
Following this course's dependency-injection testing pattern (used throughout Units 8 and 9), a @function_tool-decorated function's underlying logic can be tested directly, without running it through an agent or making any model call at all.
def test_get_order_status_returns_correct_status():
order_statuses = {"4471": "shipped", "5502": "processing"}
def get_order_status_logic(order_id: str) -> str:
return order_statuses.get(order_id, "No order found with that ID.")
assert get_order_status_logic("4471") == "shipped"
assert get_order_status_logic("9999") == "No order found with that ID."
print("PASS: order status lookup returns the correct result for known and unknown IDs")
test_get_order_status_returns_correct_status()
Decorating a function with @function_tool wraps it for use by an agent, but the function's actual business logic is still ordinary Python underneath — testing that logic directly, exactly as Unit 8's tests exercised the plain functions behind its own tool schemas, verifies correctness without needing a real agent run or a real model call for every test.
Common Mistakes
Writing a vague or missing docstring for a @function_tool-decorated function, when that docstring is the actual description the model uses to decide when and how to call the tool, not just documentation for other developers.
Letting a tool function raise an unhandled exception, exposing raw error details rather than returning a curated, safe message the model can act on sensibly.
Assuming a tool's schema needs to be written separately from the function itself, missing the point of @function_tool, which derives the schema directly from the function's signature and docstring.
Forgetting that everything learned about tool design in Unit 8 — clear descriptions, treating arguments as untrusted, least-privilege scoping — still applies, and treating the Agents SDK as though it makes careful tool design unnecessary.
Best Practices
Write a specific, unambiguous docstring for every @function_tool-decorated function, following the same tool-description guidance Unit 8, Lesson 2 established for hand-written schemas.
Return curated, safe error messages from a tool function rather than letting exceptions propagate, applying Unit 8, Lesson 5's error-handling discipline unchanged.
Combine custom tools and built-in tools in a single agent's tools list when a task genuinely needs both, following the same reasoning Unit 9, Lesson 4 established for combining tool categories in a direct API call.
Keep a tool function's actual implementation simple and focused, letting the function's type hints and docstring do the work of communicating its interface to the model, rather than requiring a separately maintained schema.