Move from the Agents API
The Agents SDK replaces the Agents and Conversations API: client.beta.agents and client.beta.conversations of the mistralai package. With the API, you create an agent on Mistral's servers, start a conversation, run each function the model calls, and send the result back. With the SDK, an agent is a Python object. Its harness runs the loop: it calls the model, runs your tools, and records each step in a session. You run the agent in your process, then deploy it with Studio (self-hosted or Mistral-managed).
Install the SDK with uv add "mistralai-agents==1.1.0" and set MISTRAL_API_KEY: see Quickstart.
Concept by concept
| You want to | Agents API | Agents SDK |
|---|---|---|
| Create an agent | client.beta.agents.create(model=..., instructions=...) | agents.Agent(model=..., instructions=...): see Create an agent and ask it |
| Set the temperature or reasoning effort | completion_args={"temperature": 0.3} | agents.Agent(temperature=0.3, reasoning_effort=...) |
| Ask once | client.beta.conversations.start(agent_id=..., inputs="...") | await agent.run("...") |
| Ask without creating an agent | conversations.start(model=..., inputs="...") | agents.Agent(model=...): an agent in your process creates nothing on Mistral's servers |
| Let the model call your functions | A {"type": "function", ...} tool, then conversations.append() with a FunctionResultEntry | @agents.tool on the function: the harness calls it. See Call your functions |
| Get an answer that only your app has, such as the user's location | The same function tool | @agents.client_tool, answered with on_client_tool=: see Answer from your app |
| Search the web or a document library | tools=[{"type": "web_search"}], {"type": "document_library", ...} | A tool of yours that calls the hosted tool: see Hosted tools |
| Run code | tools=[{"type": "code_interpreter"}] | environment=agents.MistralSandbox(): see Run code |
| Use a connector | {"type": "connector", "connector_id": "github_app"} | connectors=[agents.connector("github_app")]: see Connectors |
| Approve tool calls first | {"type": "web_search", "tool_configuration": {"requires_confirmation": ["web_search"]}}, then append(tool_confirmations=[...]) | permission_mode="ask" or permission_policy=, answered with on_approval=: see Approve tool calls |
| Continue a conversation | conversations.append(conversation_id=..., inputs="...") | session = await agent.start_session(), then await session.run("...") for each message: see Hold a conversation |
| Continue it from another process | conversations.append() with the stored conversation_id | agent.session(session_id) on a deployed agent |
| List conversations | conversations.list() | agents.list_sessions() for a deployed agent, agents.list_sessions(host="local") for the agents of this process |
| Read the history | conversations.get_history(conversation_id=...) | The EntryCompleted events of each turn: see Read a turn's entries |
| Stream the reply | conversations.start_stream() and its message.output.delta events | on_event= and its agents.TextDelta events: see Stream the reply |
| Hand the conversation to another agent | client.beta.agents.update(agent_id=..., handoffs=[...]) | subagents=[agents.Subagent(...)]: see Hand work to other agents |
| Get a typed answer | completion_args={"response_format": {"type": "json_schema", ...}} | output_type= a Pydantic model: see Get a typed answer |
| Move stored conversations | conversations.get_history() | agent.start_session(history=...): see Move a stored conversation |
| Keep nothing on Mistral's servers | store=False | Run the agent in your process: its sessions stay in its memory |
| Pin or roll back a version | client.beta.agents.update_version(agent_id=..., version=...) | Deploy the commit you want: see Versions |
| Restart from an earlier entry | conversations.restart(conversation_id=..., from_entry_id=...) | start_session(history=) with the entries up to it: see Restart from an earlier entry |
Create an agent and ask it
With the API, you create the agent, then start a conversation with its id:
agent = client.beta.agents.create(
model="mistral-large-4",
name="lyon-guide",
instructions="You are a travel guide for Lyon. Answer in one short sentence.",
completion_args={"temperature": 0.3},
)
response = client.beta.conversations.start(agent_id=agent.id, inputs="Name one museum in Lyon.")
print(response.outputs[-1].content) # thinking and text chunks, with mistral-large-4With the SDK, the agent is an object in your code, and run() returns the reply. Save this file as guide.py:
import asyncio
from mistralai import agents
guide = agents.Agent(
model="mistral-large-4",
name="lyon-guide",
instructions="You are a travel guide for Lyon. Answer in one short sentence.",
temperature=0.3,
)
async def create() -> None:
result = await guide.run("Name one museum in Lyon.")
print(result.text)
asyncio.run(create())result.text is the reply's text, without the model's thinking. run() returns a record that says how the turn ended: see One call: agent.run(). A deployed agent keeps this name.
Each SDK block below adds to guide.py. Put it in place of the file's last asyncio.run(...) line.
Call your functions
With the API, your code finds the function call in the response, runs the function, and sends its result back:
response = client.beta.conversations.start(
model="mistral-large-4",
instructions="Use your tools. Answer in one short sentence.",
tools=[weather_tool], # {"type": "function", "function": {"name": "get_weather", ...}}
inputs="What is the weather in Lyon?",
)
call = next(entry for entry in response.outputs if isinstance(entry, FunctionCallEntry))
result = get_weather(**json.loads(call.arguments))
response = client.beta.conversations.append(
conversation_id=response.conversation_id,
inputs=[FunctionResultEntry(tool_call_id=call.tool_call_id, result=result)],
)With the SDK, decorate the function with @agents.tool. The model reads its name, docstring and typed parameters, and the harness calls it:
@agents.tool
def get_weather(city: str) -> str:
"""The weather in a city today."""
return f"Sunny, 21 °C in {city}."
weather_guide = agents.Agent(
model="mistral-large-4",
instructions="Use your tools. Answer in one short sentence.",
tools=[get_weather],
)
async def tools() -> None:
result = await weather_guide.run("What is the weather in Lyon?")
print(result.text)
asyncio.run(tools())See Tools.
Answer from your app
Some answers exist only in the app that runs the agent, such as the user's location. Declare such a function as a client tool. Bind the function that answers it with .answer, and pass that function to run() in on_client_tool=agents.ClientToolAnswers(...):
@agents.client_tool
def get_city() -> str:
"""The user's current city."""
raise NotImplementedError # the app that runs the agent answers it
@get_city.answer
def read_city() -> str:
return "Lyon" # in a real app: ask the device
located_guide = agents.Agent(
model="mistral-large-4",
instructions="Use your tools. Answer in one short sentence.",
tools=[get_city],
)
async def client_tool() -> None:
result = await located_guide.run("Which city am I in?", on_client_tool=agents.ClientToolAnswers(read_city))
print(result.text)
asyncio.run(client_tool())See Client tools.
Hosted tools, code and connectors
Hosted tools
With the API, you add a hosted tool such as web_search to tools:
response = client.beta.conversations.start(
model="mistral-large-4",
instructions="Answer in one short sentence.",
tools=[{"type": "web_search"}],
inputs="On which days is the Musée des Confluences in Lyon closed?",
)With the SDK, call the hosted tool from a tool of yours through the Conversations API. This tool asks mistral-large-4 with web_search, and returns the reply's text:
import os
from mistralai.client import Mistral
from mistralai.client import models as api_models
client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
def text_of(content: str | list[object]) -> str:
"""The text of an Agents API message, without the model's thinking."""
if isinstance(content, str):
return content
return "".join(chunk.text for chunk in content if isinstance(chunk, api_models.TextChunk))
@agents.tool
async def web_search(query: str) -> str:
"""Search the web."""
response = await client.beta.conversations.start_async(
model="mistral-large-4", inputs=query, tools=[{"type": "web_search"}], store=False
)
replies = [entry for entry in response.outputs if isinstance(entry, api_models.MessageOutputEntry)]
return text_of(replies[-1].content)
searcher = agents.Agent(
model="mistral-large-4",
instructions="Use web_search for current facts. Answer in one short sentence.",
tools=[web_search],
)
async def search() -> None:
result = await searcher.run("On which days is the Musée des Confluences in Lyon closed?")
print(result.text)
asyncio.run(search())For a document library, pass tools=[{"type": "document_library", "library_ids": [...]}] in the same call. For image_generation, return the image URL: with mistral-large-4, it is json.loads(entry.info["result"])["url"], for the response's tool.execution entry.
Run code
With the API, the model runs code with the code_interpreter tool:
response = client.beta.conversations.start(
model="mistral-large-4",
instructions="Run Python to compute. Answer with the number only.",
tools=[{"type": "code_interpreter"}],
inputs="What is the 30th Fibonacci number?",
)With the SDK, the code_interpreter tool becomes the agent's environment: a container in which the model runs Python and shell commands. agents.MistralSandbox() is one that Mistral runs:
analyst = agents.Agent(
model="mistral-large-4",
instructions="Run Python to compute. Answer with the number only.",
environment=agents.MistralSandbox(),
)
async def code() -> None:
result = await analyst.run("What is the 30th Fibonacci number?")
print(result.text)
asyncio.run(code())The Mistral sandbox needs the koyeb extra, uv add "mistralai-agents[koyeb]==1.1.0", and the cloud_user workspace role: see Mistral sandbox.
Connectors
With the API, a connector is a tool with its id:
response = client.beta.conversations.start(
model="mistral-large-4",
instructions="Answer in one short sentence.",
tools=[{"type": "connector", "connector_id": "github_app"}],
inputs="Which GitHub user am I?",
)With the SDK, add a connector by its name. It acts as the user who runs the session, so a user who has not connected it gets a sign-in first:
github_helper = agents.Agent(
model="mistral-large-4",
instructions="Answer in one short sentence.",
connectors=[agents.connector("github_app")],
)
async def connector() -> None:
state = await github_helper.run("Which GitHub user am I?")
match state:
case agents.Completed(text=text):
print(text)
case agents.Blocked(callbacks=callbacks): # the user has not connected GitHub yet
print("waiting on:", [callback.kind for callback in callbacks])
await github_helper.session(state.session_id).cancel()
asyncio.run(connector())See Connectors and MCP, which also adds your own MCP servers.
Approve tool calls
With the API, requires_confirmation makes the calls of a built-in tool wait, and append() answers them with tool_confirmations:
response = client.beta.conversations.start(
model="mistral-large-4",
instructions="Answer in one short sentence.",
tools=[{"type": "web_search", "tool_configuration": {"requires_confirmation": ["web_search"]}}],
inputs="On which days is the Musée des Confluences in Lyon closed?",
)
pending = [entry for entry in response.outputs if isinstance(entry, FunctionCallEntry)]
response = client.beta.conversations.append(
conversation_id=response.conversation_id,
tool_confirmations=[{"tool_call_id": call.tool_call_id, "confirmation": "allow"} for call in pending],
)With the SDK, they become a permission mode and an approval function. With permission_mode="ask", every tool call waits for an answer, and on_approval= gives it:
careful_guide = agents.Agent(
model="mistral-large-4",
instructions="Use your tools. Answer in one short sentence.",
tools=[get_weather],
permission_mode="ask", # every tool call waits for an answer
)
def approve(ctx: agents.ApprovalCallbackContext, request: agents.ApprovalRequest) -> agents.AllowApproval:
print("approve:", request.tool.name, request.input)
return agents.AllowApproval()
async def approvals() -> None:
result = await careful_guide.run("What is the weather in Lyon?", on_approval=approve)
print(result.text)
asyncio.run(approvals())To ask for some tools only, set a permission_policy. To let a person decide later, leave the approval open and answer it by id: see Approvals.
Hold a conversation
A conversation becomes a session. With the API, each message goes to append() with the conversation's id:
response = client.beta.conversations.start(agent_id=agent.id, inputs="Name one museum in Lyon.")
response = client.beta.conversations.append(conversation_id=response.conversation_id, inputs="When does it open?")With the SDK, start a session and send it each message:
async def conversation() -> None:
session = await guide.start_session()
first = await session.run("Name one museum in Lyon.")
print(first.text)
second = await session.run("When does it open?")
print(second.text)
await session.cancel()
asyncio.run(conversation())session.cancel() ends the session. Where the session lives depends on the agent:
- An agent you build with
agents.Agent(...)keeps its sessions in your process, until the event loop that started them ends. - An agent you deploy with Studio, loaded with
agents.Agent.load_from(agent_name=...), has its sessions stored by Mistral. Any of your processes reopens one by its id withagent.session(session_id), and lists them withagents.list_sessions().
See Sessions.
Stream the reply
With the API, you stream with start_stream() and read its events:
events = client.beta.conversations.start_stream(model="mistral-large-4", inputs="Name one museum in Lyon.")
for event in events:
if event.event == "message.output.delta":
print(event.data.content, end="")With the SDK, pass a function as on_event=. It gets each event of the turn while run() waits:
def show(event: agents.Event) -> None:
match event:
case agents.TextDelta(text=text):
print(text, end="", flush=True)
case agents.TurnEnded():
print()
async def stream() -> None:
await guide.run("Name one museum in Lyon.", on_event=show)
asyncio.run(stream())Each API event has an SDK event:
| Agents API event | Agents SDK event |
|---|---|
conversation.response.started | agents.TurnStarted |
message.output.delta | agents.TextDelta, the new text of a message |
tool.execution.started, tool.execution.done | agents.EntryAdded, then agents.EntryCompleted, with an agents.ToolCallEntry and then an agents.ToolResultEntry |
function.call.delta | agents.EntryCompleted with the whole agents.ToolCallEntry |
agent.handoff.started, agent.handoff.done | An agents.ToolCallEntry named agents.spawn: the agent started a subagent |
conversation.response.done, conversation.response.error | agents.TurnEnded, whose state is the turn's record, such as agents.Completed or agents.Failed |
See Events.
Hand work to other agents
Handoffs become subagents. With the API, the planner passes the conversation to the hotels agent, which answers the user:
hotels = client.beta.agents.create(
model="mistral-large-4",
name="hotels",
description="Finds a hotel with free rooms in a city.",
instructions="Recommend the Hôtel des Célestins, rooms from 120 EUR.",
)
planner = client.beta.agents.create(
model="mistral-large-4", name="planner", instructions="You plan trips. Hand hotel requests to the hotels agent."
)
planner = client.beta.agents.update(agent_id=planner.id, handoffs=[hotels.id])
response = client.beta.conversations.start(agent_id=planner.id, inputs="Find me a hotel in Lyon.")With the SDK, declare the hotels agent in the planner. A subagent reports back to its parent, and the parent answers the user:
@agents.tool
def find_hotel(city: str) -> str:
"""A hotel with free rooms in a city."""
return f"Hôtel des Célestins, {city}: rooms from 120 EUR."
planner = agents.Agent(
model="mistral-large-4",
instructions="You plan trips. Ask the hotels agent for a hotel, then answer in one short sentence.",
subagents=[
agents.Subagent(
name="hotels",
description="Finds a hotel with free rooms in a city.",
instructions="Call find_hotel once, then answer with the hotel and its price.",
tools=[find_hotel],
)
],
)
async def subagents() -> None:
result = await planner.run("Find me a hotel in Lyon.")
print(result.text)
asyncio.run(subagents())The parent can start several subagents at once and send each follow-up messages. To let another agent take over the conversation, as a handoff did, make it its own agents.Agent. Send the user's next messages to a session of that agent, started with start_session(history=...) to carry the conversation over: see Move a stored conversation. See Subagents.
Get a typed answer
With the API, you pass a JSON schema as response_format and parse the reply:
response = client.beta.conversations.start(
model="mistral-medium-latest",
instructions="You are a travel guide for Lyon.",
inputs="Name one museum in Lyon.",
completion_args={
"response_format": {
"type": "json_schema",
"json_schema": {"name": "museum", "schema": Museum.model_json_schema(), "strict": True},
}
},
)
museum = Museum.model_validate_json(response.outputs[-1].content)With the SDK, pass the Pydantic model as output_type=. A Completed record's output is the parsed object:
from pydantic import BaseModel
class Museum(BaseModel):
name: str
district: str
typed_guide = agents.Agent(model="mistral-medium-latest", instructions="You are a travel guide for Lyon.")
async def typed() -> None:
state = await typed_guide.run("Name one museum in Lyon.", output_type=Museum)
match state:
case agents.Completed(output=Museum() as museum):
print(museum.name, "-", museum.district)
case _:
print(f"no museum: {state!r}")
asyncio.run(typed())See Structured output.
Move a stored conversation
To go on with a conversation stored by the API, read its entries with get_history() and pass them to start_session(history=). This function turns the entries into the SDK's history entries. It keeps messages, function calls and their results. It leaves out the entries of hosted tools and handoffs; the model still reads the replies that used them:
import json
def to_history(entries: list[api_models.Entry]) -> list[dict[str, object]]:
"""Turn the entries of conversations.get_history() into start_session(history=) entries."""
history: list[dict[str, object]] = []
for entry in entries:
match entry:
case api_models.MessageInputEntry() | api_models.MessageOutputEntry():
text = text_of(entry.content)
history.append({"type": "message", "role": entry.role, "content": [{"type": "text", "text": text}]})
case api_models.FunctionCallEntry():
arguments = json.loads(entry.arguments) if isinstance(entry.arguments, str) else entry.arguments
history.append(
{
"type": "tool_call",
"tool_call_id": entry.tool_call_id,
"name": entry.name,
"arguments": arguments,
}
)
case api_models.FunctionResultEntry():
history.append({"type": "tool_result", "tool_call_id": entry.tool_call_id, "result": entry.result})
return history
async def import_conversation(conversation_id: str) -> None:
entries = client.beta.conversations.get_history(conversation_id=conversation_id).entries
session = await guide.start_session(history=to_history(entries))
state = await session.run("Which museum did you name?")
print(state.text)
await session.cancel()For a conversation that named the Musée des Beaux-Arts, import_conversation(conversation_id) prints:
text_of comes from Hosted tools. history= works for an agent that runs in your process. See History entries.
Versions and restarts
An API agent keeps its versions, and update_version() picks the one that conversations use. A deployed SDK agent is your code at one commit: each deploy of a new commit under the same name replaces the agent, and its open sessions run their next message on the new code. To roll back, deploy the earlier commit. See Update a deployed agent.
Restart from an earlier entry
conversations.restart(conversation_id=..., from_entry_id=..., inputs=...) starts a new conversation that keeps the entries up to that entry, then sends inputs. With the SDK, start a session with those entries, with to_history of Move a stored conversation, then send the message:
async def restart_from(conversation_id: str, entry_id: str, inputs: str) -> None:
entries = client.beta.conversations.get_history(conversation_id=conversation_id).entries
kept = entries[: [entry.id for entry in entries].index(entry_id) + 1]
session = await guide.start_session(history=to_history(kept))
state = await session.run(inputs)
print(state.text)
await session.cancel()For a conversation that named a museum, then a park, a restart from the museum reply with "Name one café near it." prints:
Next
- Quickstart: build an agent step by step.
- Deploy your agent: deploy it with Studio (self-hosted or Mistral-managed) and call it from your app.
- What an agent is: everything an agent can have.