Leem

Runs

Flux d'événements (SSE)

Suivre un run en direct : événements, reprise sur coupure, exemple Node.

Passez "stream": true à POST /v1/runs pour suivre un run en direct plutôt que d'interroger GET /v1/runs/{id} en boucle. La réponse est un flux d'événements serveur (SSE, text/event-stream) : chaque écriture du serveur est lue par le worker qui exécute le run, jamais générée à la volée ; une coupure de la connexion n'arrête jamais le run, elle arrête seulement votre lecture.

En-têtes de la réponse

yaml
Content-Type: text/event-stream; charset=utf-8
Cache-Control: no-cache, no-transform
X-Accel-Buffering: no
Leem-Run-Id: run_01k6exampler000000000000

Leem-Run-Id donne l'id du run dès l'ouverture du flux : utile pour reprendre plus tard avec GET /v1/runs/{run}/events, ou pour appeler GET /v1/runs/{run} en secours.

Format d'un événement

vbnet
id: 14
event: text.delta
data: {"type":"text.delta","run_id":"run_01k6exampler000000000000","round":1,"delta":"Le ticket"}

Chaque événement tient sur trois lignes (id:, event:, data:) suivies d'une ligne vide. id est le numéro de séquence de l'événement dans ce run (seq, strictement croissant) : c'est ce que vous renvoyez pour reprendre le flux après une coupure. data est toujours un objet JSON dont type répète le nom de l'événement.

Une ligne de commentaire (: ping) est envoyée toutes les 15 secondes sans activité, pour garder la connexion ouverte à travers les intermédiaires HTTP ; ignorez toute ligne commençant par :.

Événements

ÉvénementQuand
run.startedLe run quitte la file et commence à s'exécuter.
step.startedUn appel d'outil commence.
step.completedUn appel d'outil se termine (réussi ou en échec).
text.deltaUn fragment de texte de la réponse.
text.resetLe texte produit jusqu'ici est abandonné (le modèle reprend son tour) : videz ce que vous aviez affiché.
chart.createdUn graphique a été produit.
file.createdUn fichier a été produit.
run.requires_actionLe run attend les résultats d'un outil client ; porte required_action (les appels à exécuter). Le flux se ferme juste après, tant que le run attend.
run.completedLe run a réussi ; dernier événement, il porte le run complet.
run.failedLe run a échoué ; dernier événement, il porte le run complet avec son error.
run.cancelledLe run a été annulé ; dernier événement, il porte le run complet.

run.completed, run.failed et run.cancelled sont terminaux : aucun événement ne les suit, et leur data.run est la même forme que GET /v1/runs/{id} (voir Lancer un run).

run.requires_action n'est pas terminal : le run repart quand vous envoyez vos résultats à POST /v1/runs/{run}/tool_outputs. Tant qu'il attend, le serveur ferme le flux après cet événement (rien d'autre ne viendrait) ; reconnectez-vous ensuite avec Last-Event-ID pour lire la suite (step.completed, text.delta, puis l'événement terminal). Votre client doit donc distinguer une fermeture après run.requires_action (renvoyez les résultats, puis reprenez) d'une fermeture après un événement terminal.

Reprendre après une coupure

Une connexion peut se couper (réseau, redémarrage d'un intermédiaire, ou la limite de 15 minutes d'un flux ouvert côté serveur). Reconnectez-vous avec le dernier id reçu, par l'en-tête standard Last-Event-ID ou, si votre client ne le permet pas, par ?after= :

bash
GET /v1/runs/run_01k6exampler000000000000/events
Last-Event-ID: 14
bash
GET /v1/runs/run_01k6exampler000000000000/events?after=14

Le flux reprend à partir de l'événement suivant : aucun doublon, aucun événement manqué. Un run déjà terminal renvoie directement ses événements restants puis se ferme.

Capacité

Le nombre de flux ouverts en même temps sur la plateforme est limité. Au-delà, POST /v1/runs (avec stream: true) ou GET /v1/runs/{id}/events renvoie 503 stream_capacity_reached avec un en-tête Retry-After : réessayez dans quelques secondes, ou repassez en interrogation (GET /v1/runs/{id} sans flux).

Client Node complet

js
async function streamRun(apiUrl, apiKey, runId, onEvent) {
  let lastId = 0;
  while (true) {
    const res = await fetch(`${apiUrl}/v1/runs/${runId}/events?after=${lastId}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);

    const reader = res.body.getReader();
    const decoder = new TextDecoder();
    let buffer = "";
    let terminal = false;

    try {
      while (true) {
        const { value, done } = await reader.read();
        if (done) break;
        buffer += decoder.decode(value, { stream: true });

        let sep;
        while ((sep = buffer.indexOf("\n\n")) >= 0) {
          const raw = buffer.slice(0, sep);
          buffer = buffer.slice(sep + 2);
          if (raw.startsWith(":")) continue; // heartbeat / padding comment

          const idLine = raw.match(/^id: (\d+)$/m);
          const dataLine = raw.match(/^data: (.*)$/m);
          if (!dataLine) continue;

          if (idLine) lastId = Number(idLine[1]);
          const event = JSON.parse(dataLine[1]);
          onEvent(event);
          if (["run.completed", "run.failed", "run.cancelled"].includes(event.type)) {
            terminal = true;
          }
          if (event.type === "run.requires_action") {
            // Execute event.required_action.tool_calls on your side and POST them to
            // /v1/runs/{id}/tool_outputs; the loop then reconnects from lastId.
          }
        }
      }
    } finally {
      reader.releaseLock();
    }

    if (terminal) return; // the run is over: nothing more will ever come
    // Otherwise the connection ended (server-side time limit, network drop):
    // reconnect from lastId — the loop above does exactly that.
  }
}

Équivalent curl

bash
curl -N https://leem-api.lcom-corp.com/v1/runs/run_01k6exampler000000000000/events \
  -H "Authorization: Bearer $LEEM_API_KEY" \
  -H "Last-Event-ID: 14"

-N désactive la mise en tampon de curl, indispensable pour voir les événements arriver au fil de l'eau plutôt que d'un bloc à la fin.