Tools
A tool lets the model call your Python code, such as an API call, a database query or a calculation. The SDK describes the tool to the model, runs the function when the model calls it, and sends the result back.
Write a tool
Decorate a function with @agents.tool and pass it in tools=. Here the agent reads an order with it:
import asyncio
from mistralai import agents
@agents.tool
async def get_order(order_id: str) -> dict[str, str]:
"""Get the status of an order."""
return {"order_id": order_id, "status": "shipped on Monday"} # read from your database
agent = agents.Agent(
model="mistral-large-4",
instructions="Answer questions about orders in one plain sentence.",
tools=[get_order],
)
async def main() -> None:
state = await agent.run("Has order 1042 shipped?")
if isinstance(state, agents.Completed):
print(state.text)
print(state.tool_calls)
if __name__ == "__main__":
asyncio.run(main())The model called get_order once, and the turn's record lists the call under the tool's id, client.get_order: client is the default namespace of your own tools.
The SDK builds what the model reads from the function itself. It takes the name from the function's name, the description from its docstring, and the input schema from its typed parameters. Annotate the return type too: it becomes the output schema.
The decorated function stays callable, so you can test it like any other function with await get_order("1042"). Define tools at module level, since the decorator raises ValueError for a nested function or a lambda.
Do not block in an async def tool, for example with time.sleep. Write a blocking tool as a plain def, which runs in its own thread.
Describe the arguments
Describe an argument to the model with Annotated and a Pydantic Field:
from typing import Annotated
from pydantic import Field
from mistralai import agents
@agents.tool
async def search_issues(
query: Annotated[str, Field(description="Full-text query, such as 'login timeout'.")],
limit: int = 10,
) -> list[str]:
"""Search the issue tracker."""
return [f"#{n}: {query}" for n in range(limit)] # call your tracker hereEvery parameter needs a type annotation. Pass name=, description= or input_schema= to override what the function gives.
Report a failure to the model
A failure you expect, such as an unknown invoice, is part of the tool's answer. Raise agents.ToolError with a message for the model. The model reads that message as written, and can act on it:
import asyncio
from mistralai import agents
INVOICES = {"A-1": {"id": "A-1", "status": "paid"}}
@agents.tool
async def get_invoice(invoice_id: str) -> dict[str, str]:
"""Fetch an invoice."""
if invoice_id not in INVOICES:
raise agents.ToolError(f"No invoice {invoice_id}. Ask the user to check the number.")
return INVOICES[invoice_id]
agent = agents.Agent(model="mistral-large-4", instructions="Answer in one plain sentence.", tools=[get_invoice])
async def main() -> None:
state = await agent.run("What is the status of invoice B-7?")
if isinstance(state, agents.Completed):
print(state.text)
asyncio.run(main())The model asked the user to check the number, as the message told it to.
An unexpected error, any other exception, is a bug or an outage. The SDK logs it with its traceback (deployed, in your deployment's logs), and by default the model reads only The tool failed with an unexpected error., since a traceback can hold internal names or customer data. Set tool_errors="show" on the agent while you develop to show the model the exception, or tool_errors="raise" to fail the turn instead.
With "raise", your code handles the failure instead of the model:
import asyncio
from mistralai import agents
@agents.tool
async def get_region_price(region: str) -> int:
"""Price of the plan in a region."""
return {"us-east": 10}[region] # raises KeyError for any other region
agent = agents.Agent(model="mistral-large-4", tools=[get_region_price], tool_errors="raise")
async def main() -> None:
match await agent.run("What does the plan cost in eu-west?"):
case agents.Failed(error=error):
print(error.code, error.message)
#> tool_raised Tool 'get_region_price' raised KeyError
case agents.Completed(text=text):
print(text)
asyncio.run(main())error.message gives the tool's name and the exception's class, here KeyError. It leaves out the exception's own text, 'eu-west', because that text can hold internal names or customer data. The full exception and its traceback are in the SDK's log.
A result that does not match the tool's output_schema= is not an unexpected error. The SDK logs a warning. With "hide" and "show", the model reads which field is wrong. With "raise", the turn ends Failed with error code tool_output_schema_mismatch.
How the model calls a tool
By default, the model calls your tools from code it writes in the built-in run_typescript tool, as tools.client.<name>(...), so it can chain several calls in one step.
Set model_access="direct" to put a tool in the model's tool list instead:
from mistralai import agents
@agents.tool(model_access="direct")
async def get_weather(city: str) -> str:
"""Weather for a city."""
return f"Sunny in {city}."How a tool runs when deployed
Deployed with Studio (self-hosted or Mistral-managed), a tool runs at most once. After a lost call, such as a crash, the model reads that the call may or may not have taken effect, so a tool with side effects must be safe to call twice. Pass the service it calls an idempotency key built from the request, so that a second call carries the same key:
@agents.tool
async def refund_order(order_id: str, amount_cents: int) -> str:
"""Refund an order."""
refund = await payments.refunds.create( # your payment service's client
order_id=order_id,
amount_cents=amount_cents,
idempotency_key=f"refund-{order_id}", # the same key when the model calls again
)
return f"Refund {refund.id} for order {order_id}: {refund.status}."When the service takes no key, check first: read the order's refunds, and return the one you find instead of refunding again. A tool that only reads needs neither.
Client tools
A client tool is a tool answered through a callback, not within the session. Use one when the answer exists only where the agent is used, such as the user's device or your UI; otherwise use your own tool, since a client tool makes the turn wait on your program. Declare it on a stub and give it to the agent like any tool:
from typing import Literal
from pydantic import BaseModel
from mistralai import agents
class Location(BaseModel):
city: str
country: str
@agents.client_tool
def get_location(precision: Literal["city", "street"] = "city") -> Location:
"""The user's current location."""
raise NotImplementedError # answered through a callbackWhen the model calls it, the turn waits for your program's answer. See Answer a client tool, and Callbacks to answer it from a session app. API reference: agents.client_tool.
Tool, connector or skill
| You have | Use |
|---|---|
| A Python function | A tool (@agents.tool) |
| A hosted service or an MCP server | A connector or an MCP server |
| A written procedure | A skill |
Common issues
| Symptom | Fix |
|---|---|
ValueError: Decorated tools must be defined at module scope | Move the function to module level. |
agents.SdkError when the tool is defined | Annotate every parameter, and add a docstring or description=. |
The model reads The tool failed with an unexpected error. | Your tool raised. Read the traceback in your logs, or set tool_errors="show" while you develop. |
The agent has no bash or file tools | They come from the agent's environment. Give the agent one with environment=; see Environment. |
Next
Approvals: choose which tool calls wait for a person.