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

Concept by concept

You want toAgents APIAgents SDK
Create an agentclient.beta.agents.create(model=..., instructions=...)agents.Agent(model=..., instructions=...): see Create an agent and ask it
Set the temperature or reasoning effortcompletion_args={"temperature": 0.3}agents.Agent(temperature=0.3, reasoning_effort=...)
Ask onceclient.beta.conversations.start(agent_id=..., inputs="...")await agent.run("...")
Ask without creating an agentconversations.start(model=..., inputs="...")agents.Agent(model=...): an agent in your process creates nothing on Mistral's servers
Let the model call your functionsA {"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 locationThe same function tool@agents.client_tool, answered with on_client_tool=: see Answer from your app
Search the web or a document librarytools=[{"type": "web_search"}], {"type": "document_library", ...}A tool of yours that calls the hosted tool: see Hosted tools
Run codetools=[{"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 conversationconversations.append(conversation_id=..., inputs="...")session = await agent.start_session(), then await session.run("...") for each message: see Hold a conversation
Continue it from another processconversations.append() with the stored conversation_idagent.session(session_id) on a deployed agent
List conversationsconversations.list()agents.list_sessions() for a deployed agent, agents.list_sessions(host="local") for the agents of this process
Read the historyconversations.get_history(conversation_id=...)The EntryCompleted events of each turn: see Read a turn's entries
Stream the replyconversations.start_stream() and its message.output.delta eventson_event= and its agents.TextDelta events: see Stream the reply
Hand the conversation to another agentclient.beta.agents.update(agent_id=..., handoffs=[...])subagents=[agents.Subagent(...)]: see Hand work to other agents
Get a typed answercompletion_args={"response_format": {"type": "json_schema", ...}}output_type= a Pydantic model: see Get a typed answer
Move stored conversationsconversations.get_history()agent.start_session(history=...): see Move a stored conversation
Keep nothing on Mistral's serversstore=FalseRun the agent in your process: its sessions stay in its memory
Pin or roll back a versionclient.beta.agents.update_version(agent_id=..., version=...)Deploy the commit you want: see Versions
Restart from an earlier entryconversations.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

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-4

With 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())
Output
The Musée des Confluences is a striking museum in Lyon at the confluence of the Rhône and Saône rivers.

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

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())
Output
The weather in Lyon is sunny, 21 °C.

See Tools.

Answer from your app

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())
Output
You're in Lyon.

See Client tools.

Hosted tools, code and connectors

Hosted tools, code and connectors

Hosted tools

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())
Output
The Musée des Confluences is closed every Monday (except during Zone A school holidays, Easter Monday, and Whit Monday), as well as on January 1, May 1, and December 25.

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

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())
Output
832040

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

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())
Output
waiting on: ['sign_in']

See Connectors and MCP, which also adds your own MCP servers.

Approve tool calls

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())
Output
approve: tools.client.get_weather {'city': 'Lyon'}
The weather in Lyon today is sunny with a temperature of 21 °C.

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

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())
Output
The Musée des Beaux-Arts de Lyon is one of the city's most famous museums.
It's open Wednesday-Monday from 10:00 to 18:00 (until 18:45 on Fridays) and closed Tuesdays.

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 with agent.session(session_id), and lists them with agents.list_sessions().

See Sessions.

Stream the reply

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())
Output
The Musée des Beaux-Arts de Lyon, located on Place des Terreaux, is one of France's finest art museums.

Each API event has an SDK event:

Agents API eventAgents SDK event
conversation.response.startedagents.TurnStarted
message.output.deltaagents.TextDelta, the new text of a message
tool.execution.started, tool.execution.doneagents.EntryAdded, then agents.EntryCompleted, with an agents.ToolCallEntry and then an agents.ToolResultEntry
function.call.deltaagents.EntryCompleted with the whole agents.ToolCallEntry
agent.handoff.started, agent.handoff.doneAn agents.ToolCallEntry named agents.spawn: the agent started a subagent
conversation.response.done, conversation.response.erroragents.TurnEnded, whose state is the turn's record, such as agents.Completed or agents.Failed

See Events.

Hand work to other agents

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())
Output
I found you a hotel in Lyon: Hôtel des Célestins, with rooms from 120 EUR per night.

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

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())
Output
Musée des Beaux-Arts de Lyon - Presqu'île

See Structured output.

Move a stored conversation

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:

Output
I named the Musée des Beaux-Arts de Lyon.

text_of comes from Hosted tools. history= works for an agent that runs in your process. See History entries.

Versions and restarts

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

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:

Output
A great option right on Place des Terreaux, steps from the museum, is Café des Négociants.