Événements

Pendant l'exécution d'un tour, la session envoie des événements : le démarrage du tour, le message de l'utilisateur, chaque appel d'outil et son résultat, la réponse du modèle au fil de sa rédaction, une question dont le tour attend la réponse, puis la fin du tour. Le SDK les lit pour vous et fournit chacun sous forme de valeur typée. Suivez-les pendant l'attente pour afficher la progression de l'agent dans une conversation ou un tableau de bord, ou relisez-les plus tard.

Types d'événements

Types d'événements

ÉvénementQuandContenu
agents.TurnStartedUn tour a démarré.Rien de plus.
agents.TurnUpdatedLe statut du tour a changé.turn_id et status : "blocked" pendant l'attente d'un callback, puis "running" à la reprise, "terminated" une fois le tour terminé, ou un agents.UnknownValue pour un statut ajouté après votre version du SDK.
agents.CallbackOpenedLe tour attend une réponse, par exemple une approbation d'outil.callback : la question, comme dans le champ callbacks d'un enregistrement Blocked.
agents.CallbackResolvedLa question a reçu une réponse.callback_id
agents.TurnEndedLe tour est terminé.state : son enregistrement, Completed, Interrupted, Failed ou UnknownOutcome.
agents.SessionUpdatedLa session a changé : son statut ou son titre a été modifié, ou elle a été archivée.status : "running" lorsqu'un tour est actif, "waiting" lorsqu'aucun ne l'est, "terminated" une fois la session terminée, ou un agents.UnknownValue ; title ; archived.
agents.NoticeUne notification qui vous est destinée, et non au modèle, par exemple pour signaler un connecteur dont les outils sont actuellement indisponibles.message et level ; la documentation de référence de Notice répertorie les autres champs.
agents.EntryAddedUne entrée a été ajoutée à l'historique de la session : un message, un appel d'outil, le résultat d'un outil ou une autre entrée. L'entrée peut encore être incomplète, par exemple un message en cours de rédaction par le modèle.entry : un agents.MessageEntry (role, content et text), un agents.ContextMessage (une entrée de message avec role="context", ainsi que kind et source), un agents.ToolCallEntry, un agents.ToolResultEntry ou un agents.UnknownEntry.
agents.TextDeltaLe modèle a rédigé une nouvelle partie d'un message, après son événement EntryAdded et avant son événement EntryCompleted.entry_id, l'id de l'entrée du message, et text, le nouveau texte.
agents.EntryCompletedUne entrée de l'historique de la session est complète.entry, comme dans EntryAdded, dans sa version définitive.
agents.UnknownEventUn événement d'un type ajouté après votre version du SDK.type et raw, l'événement non analysé.

Chaque événement possède un id, sa position dans les événements de la session, et un turn_id, le tour dans lequel il s'est produit, ou None entre les tours ou lorsque l'événement n'indique pas son tour. Le premier TextDelta d'un message possède l'id de son EntryAdded, et une lecture avec after= défini sur cet id commence après ces deux événements. Chaque autre événement possède son propre id. De nouveaux types d'événements sont introduits dans les versions mineures. Ils sont représentés par UnknownEvent tant que vous n'avez pas mis à jour le SDK : ignorez les types que vous ne connaissez pas.

L'historique de la session est transmis sous forme d'événements d'entrée : le message de l'utilisateur, juste avant son TurnStarted, chaque message de contexte reçu par le modèle avec l'entrée d'un tour, chaque appel d'outil, chaque résultat d'outil, y compris celui d'un outil côté client, et chaque message du modèle. Chaque entrée est transmise une fois sous forme de EntryAdded lors de son ajout, puis une fois sous forme de EntryCompleted lorsqu'elle est complète, sur tous les hôtes, en direct comme en relecture. Une lecture qui commence après le EntryAdded d'une entrée ne reçoit que son EntryCompleted. Conservez l'entrée de EntryCompleted : seule cette version est définitive. Pendant que le modèle rédige un message, le nouveau texte est transmis sous forme d'événements TextDelta entre ces deux événements. Un message de contexte reçu par le modèle avec l'entrée d'un tour est transmis sous forme d'entrée agents.ContextMessage : voir Entrées de l'historique. Un type d'entrée pour lequel votre version du SDK ne possède pas de classe, comme le raisonnement du modèle ou un message d'un autre agent, est transmis sous forme de agents.UnknownEntry, avec l'entrée non analysée dans raw. Les id ci-dessous ne sont pas consécutifs : traitez-les comme des positions, et non comme des compteurs.

Pour afficher chaque appel d'outil terminé pendant l'exécution du tour, par exemple dans une barre de progression, suivez les événements EntryCompleted dont l'entrée est un agents.ToolResultEntry : elle contient le name de l'outil et is_error. Pour transmettre la progression à une application qui ne suit pas la session, utilisez un hook : voir Progression en direct.

Suivre un tour pendant l'attente

Suivre un tour pendant l'attente

Passez à on_event= une fonction synchrone ou asynchrone dans session.run(), turn.result() ou agent.run(). Elle reçoit les événements du tour dans l'ordre. À la fin du tour, le dernier événement est TurnEnded, dont le champ state contient l'enregistrement renvoyé par l'appel :

import asyncio

from mistralai import agents

from reviewer import payment_reviewer


def show(event: agents.Event) -> None:
    match event:
        case agents.TurnStarted():
            print(event.id, "turn started")
        case agents.CallbackOpened(callback):
            print(event.id, "waits for:", callback.title)
        case agents.CallbackResolved(callback_id):
            print(event.id, "answered:", callback_id)
        case agents.EntryAdded(entry=entry):
            print(event.id, "entry added:", type(entry).__name__)
        case agents.TextDelta(text=text):
            print(event.id, "text:", repr(text))
        case agents.EntryCompleted(entry=entry):
            print(event.id, "entry completed:", type(entry).__name__)
        case agents.TurnEnded(state):
            print(event.id, "turn ended:", type(state).__name__)
        case agents.SessionUpdated(status):
            print(event.id, "session:", status)
        case agents.UnknownEvent(kind):
            print(event.id, "other event:", kind)


async def main() -> None:
    session = await payment_reviewer.start_session()
    state = await session.run("Review transaction monitoring alert 4711.", on_event=show)


asyncio.run(main())
local:3f9c2e71-...:1 entry added: MessageEntry
local:3f9c2e71-...:2 entry completed: MessageEntry
local:3f9c2e71-...:3 turn started
local:3f9c2e71-...:4 session: running
local:3f9c2e71-...:5 entry added: UnknownEntry
local:3f9c2e71-...:18 entry completed: UnknownEntry
...
local:3f9c2e71-...:43 entry added: ToolCallEntry
local:3f9c2e71-...:44 entry completed: ToolCallEntry
local:3f9c2e71-...:45 entry added: ToolResultEntry
local:3f9c2e71-...:46 entry completed: ToolResultEntry
...
local:3f9c2e71-...:110 entry added: MessageEntry
local:3f9c2e71-...:110 text: 'Alert '
local:3f9c2e71-...:111 text: '4711 flags'
...
local:3f9c2e71-...:130 text: '** on it.'
local:3f9c2e71-...:131 entry completed: UnknownEntry
local:3f9c2e71-...:132 entry completed: MessageEntry
local:3f9c2e71-...:133 turn ended: Completed

Le message de l'utilisateur arrive en premier, sous forme d'entrée, puis le tour démarre. Chaque entrée du tour est transmise sous forme de EntryAdded, puis de EntryCompleted : ici, le raisonnement du modèle sous forme de UnknownEntry, puis un appel d'outil et son résultat. Le texte de la réponse est transmis sous forme d'événements TextDelta entre ses deux événements. Un tour avec permission_mode="ask" envoie aussi un CallbackOpened pour chaque approbation attendue, puis un CallbackResolved une fois la réponse reçue.

Attendez avec on_event= pour afficher les événements d'un tour pendant qu'une fonction on_approval= répond à ses demandes d'approbation. Parcourir les événements et attendre dans une même tâche bloque l'exécution lorsqu'une approbation est requise : exécutez ces opérations dans deux tâches. Pour piloter le tour et répondre à ses demandes d'approbation tout en l'affichant, voir Piloter un tour tout en le suivant.

Afficher la conversation au fil de son déroulement

Afficher la conversation au fil de son déroulement

Traitez les événements EntryCompleted pour afficher la conversation : le message de l'utilisateur, chaque appel d'outil et son résultat, et la réponse de l'agent. Pour afficher la réponse pendant que le modèle la rédige, affichez les textes de ses événements TextDelta à mesure qu'ils arrivent. TextDelta n'est transmis qu'en direct : une relecture ou une lecture commencée tardivement manque tout ou partie de ces événements. Lorsque les deltas affichés ne constituent pas la réponse complète, son EntryCompleted fournit le texte manquant : la suite du texte, ou le texte entier si les premiers deltas ont été manqués :

shown: dict[str, str] = {}  # the reply text printed so far, by entry id


def show_chat(event: agents.Event) -> None:
    match event:
        case agents.EntryCompleted(entry=agents.MessageEntry(role="user") as message):
            print("user:", message.text)
        case agents.EntryCompleted(entry=agents.ToolCallEntry(name, arguments)):
            print("calling", name, arguments)
        case agents.EntryCompleted(entry=agents.ToolResultEntry(name)):
            print(name, "returned")
        case agents.TextDelta(entry_id=entry_id, text=text):
            if entry_id not in shown:
                print("agent: ", end="")
            shown[entry_id] = shown.get(entry_id, "") + text
            print(text, end="", flush=True)  # the reply, while the model writes it
        case agents.EntryCompleted(entry=agents.MessageEntry(role="assistant") as reply):
            so_far = shown.pop(reply.id, "")
            if so_far and reply.text.startswith(so_far):
                print(reply.text[len(so_far) :])  # the rest, if the last deltas were missed
            else:  # no deltas, or the first ones missed
                print(("\n" if so_far else "") + "agent: " + reply.text)


async def chat() -> None:
    session = await payment_reviewer.start_session()
    await session.run("Review transaction monitoring alert 4711.", on_event=show_chat)
    await session.cancel()


asyncio.run(chat())
user: Review transaction monitoring alert 4711.
calling client.get_alert {'alert_id': '4711'}
client.get_alert returned
calling client.open_case {'alert_id': '4711', 'summary': 'Suspicious 9,800 EUR payment from FR76-3000-4471 ...', 'evidence': [...]}
client.open_case returned
agent: Alert 4711 flags a suspicious 9,800 EUR payment from account FR76-3000-4471 to a beneficiary added just 3
minutes earlier immediately after a password reset, with the amount sitting just under the 10,000 EUR reporting ...
Relire et suivre les événements

Relire et suivre les événements

turn.iter_events() et session.iter_events() fournissent des événements sans piloter le tour : ces méthodes ne renvoient jamais d'enregistrement et ne répondent à aucune demande. Utilisez-les pour afficher une session pilotée par un autre processus.

turn.iter_events() fournit les événements d'un tour, à partir de la fin du tour précédent, puis s'arrête à la fin de ce tour. Si le tour est terminé, la méthode relit ses événements. Ici, session et state sont ceux de main() ci-dessus :

async for event in session.turn(state.turn_id).iter_events():
    show(event)
local:3f9c2e71-...:1 entry added: MessageEntry
local:3f9c2e71-...:2 entry completed: MessageEntry
local:3f9c2e71-...:3 turn started
...
local:3f9c2e71-...:110 entry added: MessageEntry
local:3f9c2e71-...:110 text: 'Alert '
local:3f9c2e71-...:131 entry completed: UnknownEntry
local:3f9c2e71-...:132 entry completed: MessageEntry
local:3f9c2e71-...:133 turn ended: Completed

La relecture contient les événements EntryAdded et EntryCompleted de chaque entrée, comme la lecture en direct. Une relecture fournit au maximum le premier TextDelta d'une réponse : lisez le texte de la réponse dans son EntryCompleted.

session.iter_events(after=) fournit les événements de tous les tours : d'abord ceux que la session possède déjà, puis ceux qui arrivent en direct. La méthode ne s'arrête pas à la fin d'un tour :

await session.start_turn("Which case did you open? Reply with the case id only.")
async for event in session.iter_events(after=state):
    show(event)
    if isinstance(event, agents.TurnEnded):
        print(event.state.text)
        break
await session.cancel()
local:3f9c2e71-...:135 entry added: MessageEntry
local:3f9c2e71-...:136 entry completed: MessageEntry
local:3f9c2e71-...:137 turn started
local:3f9c2e71-...:138 session: running
...
local:3f9c2e71-...:149 text: 'CASE-'
local:3f9c2e71-...:150 text: '4711'
local:3f9c2e71-...:151 entry completed: UnknownEntry
local:3f9c2e71-...:152 entry completed: MessageEntry
local:3f9c2e71-...:153 turn ended: Completed
CASE-4711
Reprendre à partir d'un curseur

Reprendre à partir d'un curseur

Un curseur est une position dans les événements de la session : l'id d'un événement ou le cursor d'un enregistrement. Traitez-le comme une chaîne opaque : son format diffère entre une session exécutée dans votre processus et celle d'un agent déployé avec Studio (auto-hébergé ou géré par Mistral). Dans chaque appel, after= accepte un curseur ou un enregistrement. Les événements commencent juste après cette position, sans doublon ni omission, à l'exception de TextDelta, qui n'est transmis qu'en direct (voir Afficher la conversation au fil de son déroulement) :

  • after=state commence la lecture à la position où l'enregistrement state a été créé. Le deuxième tour ci-dessus commence après l'enregistrement du premier tour.
  • after=event.id commence la lecture après cet événement. Stockez l'id du dernier événement affiché avec l'identifiant de la session. Pour un agent déployé, un processus redémarré reprend à cette position avec agent.session(session_id).iter_events(after=stored_id). Une session exécutée dans votre processus s'arrête lorsque la boucle d'événements qui l'a démarrée se ferme : voir Environnement d'exécution local. Après un TurnEnded, stockez son id, et non son state.

Sans after=, session.iter_events() commence au début de la session, et turn.iter_events() après la fin du tour précédent.

on_event= reçoit les événements jusqu'à l'enregistrement renvoyé par l'appel. Un appel suivant avec after= défini sur cet enregistrement ne manque donc aucun événement, sauf TextDelta, et n'en répète aucun. Dans une boucle de turn.result(after=state, timeout=...), un appel qui se termine pendant que le modèle rédige une réponse ne transmet pas les événements TextDelta restants de cette réponse : affichez la réponse à partir de son EntryCompleted, qui contient le texte complet.