# Streaming SSE — contrat des chunks (completion agent) Ce document décrit le format des événements envoyés par l’endpoint de completion en **Server-Sent Events** (`text/event-stream`), après extension pour les événements **`custom`** émis depuis les outils via `get_stream_writer()` côté serveur. ## Format général Chaque événement est une ligne : ```text data: \n\n ``` Le JSON est un objet ; les champs historiques restent : | Champ | Rôle | |--------|------| | `content` | Fragment de texte assistant (deltas) ; souvent vide sur le chunk final ou sur les événements non texte. | | `done` | `false` pendant le flux ; `true` sur le **dernier** chunk (réponse terminée). | | `metadata` | Modèle, agent, usage cumulé, etc. ; le chunk final inclut aussi `usage_by_model`, `latency_s` et champs additionnels post-traitement. | | `documents` | Liste de références documentaires accumulées pendant le run. | | `conversation_id` | Ajouté par la route HTTP sur chaque chunk lorsqu’il est fourni dans la requête. | | `error` | Présent uniquement en cas d’erreur côté serveur sur le flux (chunk terminal avec message d’erreur). | ## Discrimination : `chunk_kind` Les clients **récents** doivent utiliser `chunk_kind` pour séparer le texte modèle des événements applicatifs : | `chunk_kind` | Signification | |----------------|---------------| | `text` | Delta de texte assistant ; concaténer `content` au buffer de réponse. | | `custom` | Données émises par un outil ou un nœud via `get_stream_writer()` ; **ne pas** traiter `content` comme du texte modèle. | | `final` | Chunk terminal (`done: true`) ; `content` est en général vide ; lire `metadata` pour usage, latence, etc. | Les clients **anciens** qui ignorent les champs inconnus continuent de fonctionner : ils concatènent `content` quand il est non vide et finalisent sur `done: true`. Les événements `custom` ont `content: ""` : ils n’introduisent pas de texte parasite. ### Objet `custom` (lorsque `chunk_kind === "custom"`) Le champ `custom` est un objet JSON avec au minimum : - `kind` : sous-type côté métier (ex. `"tool"`, `"retrieval"`, `"custom"` pour les payloads non étiquetés). Les autres clés sont **libres** et peuvent évoluer ; le client doit être défensif (`optional chaining`, pas de schéma strict imposé par l’API). Exemple (statut d’outil) : ```json { "chunk_kind": "custom", "custom": { "kind": "tool", "tool": "check_project_id", "message": "Vérification de la présence d'un projet" }, "content": "", "done": false, "metadata": { "model": "...", "agent": "...", "usage": {} }, "documents": [] } ``` ## Algorithme de consommation recommandé 1. Découper le flux en lignes ; pour chaque ligne commençant par `data: `, parser le JSON. 2. Si `error` est défini : afficher / journaliser l’erreur et terminer la session. 3. Sinon, inspecter `chunk_kind` : - **`text`** (ou absence de `chunk_kind` sur d’anciens serveurs uniquement) : ajouter `content` au buffer assistant ; mettre à jour `metadata` / `documents` si besoin. - **`custom`** : router sur `custom.kind` (ex. afficher une progression d’outil) ; ignorer `content` pour l’affichage du modèle. - **`final`** : consolider la réponse, lire usage / latence depuis `metadata`, terminer l’UI de streaming. 4. Si `done === true` (toujours vrai sur le chunk `final`), finaliser l’état local. ## TypeScript (indicatif) ```typescript type StreamChunk = { chunk_kind?: "text" | "custom" | "final"; content: string; done: boolean; metadata?: Record; documents?: unknown[]; conversation_id?: string | null; custom?: { kind: string; [key: string]: unknown }; error?: string; }; ``` ## Tests côté client - **Rétrocompat** : flux mock sans `chunk_kind` + chunks avec uniquement du `content` et un dernier `done: true` → le rendu texte doit rester identique à l’ancien comportement. - **Enrichi** : intercaler des chunks `chunk_kind: "custom"` → le texte final (concaténation des seuls `text`) doit être identique à un flux sans custom ; les événements outil doivent être reçus et affichables séparément. ## Côté serveur (émetteurs) Tout argument passé à `get_stream_writer()` doit rester **JSON-sérialisable** (pas d’objets arbitraires non encodables). Convention : - Préférer un **objet** avec `kind` explicite. - Une **chaîne** seule est acceptée et normalisée en `{ "kind": "tool", "message": "" }` pour compatibilité avec d’anciens outils. Voir l’implémentation dans `services/stream_payloads.py` et `services/agent_service.py` (`stream`).