# Mod compteur de tokens : contrat avec MIAM (POST /economies) Destinataire : l'agent Claude Code qui travaille dans le dépôt MIAM. Le côté plugin est fait et testé (dépôt `plugin-claude-miam`, schéma 1) ; ce document décrit ce que MIAM doit recevoir, stocker et restituer. ## But Le mod estime, sans aucun coût en tokens, ce que chaque délégation évite à Claude et le coût du pilotage (consommation d'un subagent qui délègue ; le subagent `miam:delegue` est mis de côté, voir `agent-delegue.md`). MIAM connaît les tokens du modèle local mais pas ce qui se passe côté Claude : ces événements complètent son bilan. ## Prérequis dans MIAM : `demande_id` dans le journal `delegations.jsonl` ne contient pas aujourd'hui l'identifiant de la demande (`core/delegation.py`, `journal_entry`) : la jointure est impossible sans lui. Ajouter un paramètre `demande_id` à `journal_entry` et le passer depuis `McpService.deleguer` (réussites) et `_journal_failure` / `_on_request_finished` (refus, échecs, annulations, expirations ; `None` pour un refus avant mise en file, qui n'a pas d'id). C'est le `req.id` de la file (`dem-…`), celui que `deleguer` et `suivre` renvoient au client. ## Transport POST http://127.0.0.1:/economies (port = celui du serveur MCP, 8090 par défaut ; réglage `port` du plugin). En-têtes : `Content-Type: application/json` et `Authorization: Bearer ` (le même jeton que `/mcp`). Corps : `{ "evenements": [ ... ] }`, lot de 1 à 500 événements (`ATTENTE_MAX`). Le mod ne poste jamais depuis un hook : chaque événement est d'abord mis en attente dans le stockage du plugin (`$.store`, clé `a_envoyer`), puis une minuterie (toutes les 5 s, lancée au démarrage de la session) poste toute l'attente en un lot. Une délégation n'attend donc jamais MIAM. Une seule requête à la fois. Sans réponse ou statut à garder, le lot reste en attente et repart au tour suivant, avec ce qui est arrivé depuis (les plus anciens d'abord, doublons d'id retirés, 500 au plus : au-delà, les plus anciens sont perdus). Conséquence : MIAM reçoit souvent plusieurs fois le même événement, et doit répondre vite (pas de travail lourd dans la requête). ## Réponses attendues | Statut | Quand | Effet côté mod | | :--- | :--- | :--- | | 200 (ou 204) | Lot reçu | Le mod vide son attente. | | 400 ou 422 | Lot invalide | Le mod l'abandonne (pas de renvoi). | | 401 | Jeton refusé | Gardé. | | 404 | Route absente (MIAM ancien) | Gardé. | | 413 | Corps trop gros | Gardé (500 événements font environ 300 Ko : rester au-dessus de 1 Mo). | | 5xx / Connexion refusée | Erreur réseau | Gardé. | Corps de réponse recommandé pour 200 : `{ "ok": true, "recus": n, "doublons": m }` (le mod ne le lit pas ; utile aux tests et au débogage). Un lot est accepté ou refusé en entier : pas de 200 partiel avec des événements rejetés, sinon le mod les croirait reçus. ## Modèle de données ### Événement delegation | Champ | Type | Description | | :--- | :--- | :--- | | `schema` | `number` | Version du schéma (fixé à 1). | | `type` | `string` | Valeur `"delegation"`. | | `id` | `string` | Identifiant unique stable : `claude-code:`. | | `date` | `string` | ISO 8601 de la génération de l'événement. | | `client` | `string` | Valeur `"claude-code"`. | | `session_id` | `string` | Identifiant de la session Claude Code. | | `demande_id` | `string` | Id MIAM de la demande (`dem-…`), tel que `deleguer` ou `suivre` l'a renvoyé. | | `agent_id` | `string \| null` | Boucle Claude Code qui a appelé `deleguer` : id du subagent, `null` pour l'agent principal. | | `par_subagent` | `boolean` | `agent_id !== null`. | | `retour` | `string` | `"complet"` ou `"apercu"` (argument `retour` de `deleguer`). | | `tokens_entree` | `integer` | Tokens lus par le modèle local, tels que `deleguer` les a renvoyés (= journal). | | `tokens_sortie` | `integer` | Tokens écrits par le modèle local, idem. | | `estimation` | `object` | `lus`, `ecrits`, `consigne`, `rendus`, `evites` (entiers, en tokens Claude) et `methode` (`"caracteres/4"`). | Seules les délégations réussies (`ok: true`) produisent un événement ; une délégation « en file » n'en produit qu'un, quand `suivre` rend son résultat. **Calcul des estimations (fonction `estimer`) :** * `consigne` : `Math.ceil(longueur_consigne / 4)`. * `lus` : `Math.max(0, tokens_entree - consigne)`. * `ecrits` : `tokens_sortie`. * `rendus` : Si `retour === 'apercu'`, alors `Math.ceil(longueur_reponse / 4)`, sinon `ecrits`. * `evites` : `lus + ecrits - (consigne + rendus)` (si pas de subagent) ou `lus + ecrits` (si subagent : la consigne et la lecture de la réponse sont alors dans le coût du pilotage). Peut être négatif (consigne longue, petite réponse relue en entier) : ne pas le borner. ### Événement pilotage | Champ | Type | Description | | :--- | :--- | :--- | | `schema` | `number` | Version du schéma (fixé à 1). | | `type` | `string` | Valeur `"pilotage"`. | | `id` | `string` | Identifiant unique stable : `claude-code:::`. | | `date` | `string` | ISO 8601 de la génération de l'événement. | | `client` | `string` | Valeur `"claude-code"`. | | `session_id` | `string` | Identifiant de la session Claude Code. | | `agent_id` | `string` | Id du subagent, ou `"principal"` pour le suivi d'OpenCode par l'agent principal. | | `turn_id` | `string` | Id du tour du subagent (un tour = une exécution du subagent), ou `tool_use_id` de l'appel pour le suivi d'OpenCode. | | `demande_ids` | `string[]` | Les `demande_id` des délégations réussies de ce tour (au moins un). | | `modele` | `string` | Modèle Claude du subagent, tel que l'API le nomme (ex. `claude-haiku-5-5`), ou `"opencode"` pour le suivi d'OpenCode. | | `usage` | `object` | Les 4 compteurs de l'API pour ce tour (entiers) : `input_tokens`, `output_tokens`, `cache_read_input_tokens`, `cache_creation_input_tokens`. Pour le suivi d'OpenCode, usage réel de la requête de l'agent principal qui lit le résultat d'un appel de suivi (`deleguer`, `suivre`, `autoriser`, `appliquer`, `rejeter`, `annuler`, relecture du diff), contexte relu compris, au prorata des outils lancés par la requête précédente. | | `cout` | `integer` | Coût du tour en tokens Claude, à déduire des `evites` des délégations couvertes. | Le pilotage arrive à la fin du tour du subagent, donc après ses événements `delegation` (sauf renvoi : l'ordre n'est pas garanti). ### Événement correction Pondère le gain par le travail de reprise : quand l'**agent principal** relit ou retouche un fichier écrit par une délégation (champ `sortie` du résultat de `deleguer`), le coût de cet appel d'outil est déduit. Les reprises faites par le subagent ne produisent pas d'événement : elles sont déjà dans son `pilotage`. Une délégation sans `sortie` (réponse rendue dans la conversation) n'a pas de correction mesurable. | Champ | Type | Description | | :--- | :--- | :--- | | `schema` | `number` | 1. | | `type` | `string` | `"correction"`. | | `id` | `string` | `claude-code::` (unique par appel d'outil). | | `date` | `string` | ISO 8601 de l'appel. | | `client` | `string` | `"claude-code"`. | | `session_id` | `string` | Session Claude Code. | | `demande_id` | `string` | Délégation dont la sortie est reprise. | | `outil` | `string` | `Read`, `Edit`, `Write`, `NotebookEdit` ou `Bash`. | | `tool_use_id` | `string` | Id de l'appel d'outil (`reprise-` si Claude Code n'en a pas donné). | | `tokens_lus` | `integer` | Ce que Claude lit : résultat d'un `Read` (ou d'une commande `Bash`). | | `tokens_ecrits` | `integer` | Ce que Claude écrit : `old_string` + `new_string` d'un `Edit`, contenu d'un `Write`, `new_source` d'un `NotebookEdit`, commande `Bash`. | | `cout` | `integer` | `tokens_lus + tokens_ecrits`, à déduire des `evites` de `demande_id`. | Rattachement (fonction `cible`) : chemin du fichier de l'appel égal à la `sortie`, après normalisation (`\` → `/`, minuscules) ; pour `Bash`, la commande contient le chemin, ou le nom du fichier s'il fait au moins 6 caractères (heuristique : une commande qui cite ce nom sans toucher au fichier est comptée aussi). Un appel qui ne lit ni n'écrit rien (coût 0) n'est pas envoyé. **Gain net** = somme des `estimation.evites` − somme des `cout` de `pilotage` − somme des `cout` de `correction`. Le taux de reprise d'une délégation (`cout` de ses corrections / ses `evites`) mesure la qualité réelle du modèle local sur la tâche : c'est la donnée la plus utile pour classer les presets avec les tests de MIAM. **Calcul du coût (fonction `cout`) :** `input_tokens + cache_creation_input_tokens + output_tokens + Math.round(cache_read_input_tokens / 10)`. **Exemple** : une délégation du subagent `a1` (consigne de 400 caractères, réponse complète), puis la fin de son tour. Ce sont les valeurs du test `hooks/register.test.ts`, produites par le code du mod. ```json { "evenements": [ { "schema": 1, "type": "delegation", "id": "claude-code:dem-9c8a4d0189", "date": "2026-10-30T09:12:04.512Z", "client": "claude-code", "session_id": "c009a419-e17b-4036-9531-ac29eaa481d4", "demande_id": "dem-9c8a4d0189", "agent_id": "a1", "par_subagent": true, "retour": "complet", "tokens_entree": 4100, "tokens_sortie": 900, "estimation": { "lus": 4000, "ecrits": 900, "consigne": 100, "rendus": 900, "evites": 4900, "methode": "caracteres/4" } }, { "schema": 1, "type": "pilotage", "id": "claude-code:c009a419-e17b-4036-9531-ac29eaa481d4:a1:t", "date": "2026-10-30T09:12:09.031Z", "client": "claude-code", "session_id": "c009a419-e17b-4036-9531-ac29eaa481d4", "agent_id": "a1", "turn_id": "t", "demande_ids": ["dem-9c8a4d0189"], "modele": "claude-haiku-5-5", "usage": { "input_tokens": 800, "output_tokens": 200, "cache_read_input_tokens": 10000, "cache_creation_input_tokens": 0 }, "cout": 2000 } ] } ``` Lecture : 4 900 tokens évités, 2 000 de pilotage, gain net 2 900. ## Idempotence et jointure `id` est unique et stable (`delegation` : `claude-code:` ; `pilotage` : `claude-code:::` ; `correction` : `claude-code::`). Un même événement peut arriver plusieurs fois → MIAM doit ignorer un id déjà reçu et le compter dans « doublons ». Jointure avec `delegations.jsonl` par `demande_id` ; un `pilotage` couvre la liste `demande_ids`, une `correction` porte un seul `demande_id` (une délégation peut en avoir plusieurs). Un `pilotage` ou une `correction` peut arriver sans que sa délégation ait été reçue (ou l'inverse) : accepter dans tous les ordres. ## À implémenter dans MIAM 0. `demande_id` dans `delegations.jsonl` (voir « Prérequis »). 1. Route Starlette POST `/economies` montée sur la même application que `/mcp` (`McpService.http_app`), derrière `BearerAuth` (401 sans jeton valide, comme `/mcp`). Bind `127.0.0.1` inchangé. 2. Validation stricte, en cœur pur (`core/`, sans I/O) : `evenements` liste de 1 à 500 ; pour chaque événement `schema == 1`, `type` connu, champs requis présents et du bon type (entiers ≥ 0 pour les compteurs, sauf `estimation.evites` qui peut être négatif ; `agent_id` chaîne ou `null` pour `delegation`) ; champs inconnus ignorés (le mod pourra en ajouter sans changer de schéma). Un seul événement invalide → 422 pour tout le lot, avec l'index et le champ fautifs dans le corps. 3. Stockage : ajout dans `logs/mcp/economies.jsonl` (même dossier que `delegations.jsonl`), une ligne par événement nouveau : l'événement tel que reçu, plus `recu_le` (ISO). Ensemble des `id` déjà vus en mémoire, rechargé depuis le fichier au démarrage du service ; un doublon n'est pas réécrit. 4. Outil `bilan` enrichi d'une section `economies`, par `client` : `delegations` (événements `delegation`), `evites` (somme de `estimation.evites`), `pilotage` (somme des `cout` de `pilotage`), `corrections` (somme des `cout` de `correction`), `net = evites - pilotage - corrections`, et `taux_reprise = corrections / evites` (`null` si `evites` ≤ 0). Fichier absent : section à zéro. La jointure avec `delegations.jsonl` par `demande_id` permet ensuite le détail par preset ou par format (utile au choix du modèle par MIAM). 5. Tests : unitaires sur la validation (chaque champ requis, types, lot vide, 501 événements, `evites` négatif accepté) et sur le calcul du bilan (doublons, pilotage reçu avant sa délégation) ; intégration HTTP réelle comme les tests MCP existants (401 sans jeton, 200 avec `recus`/`doublons`, renvoi du même lot → tout en doublons, 422, 413). 6. Taille maximale du corps : 1 Mo → 413. 7. Optionnel : afficher le net du jour sur la carte MCP du dashboard. Hors périmètre : rien n'est à changer dans les outils MCP existants (le mod lit leurs réponses telles quelles). ## Limites de l'estimation Brief de l'agent principal et lecture du compte rendu non comptés. Coût d'un token Haiku ≠ Opus, non pondéré ; un token écrit coûte plus qu'un token lu, non pondéré non plus (les événements gardent lus et écrits séparés : MIAM peut pondérer). Estimation à 4 caractères par token. Corrections : seulement sur les fichiers `sortie`, et seulement pendant la session qui a délégué (la table des sorties n'est pas gardée d'une session à l'autre) ; une reprise ailleurs (texte de la réponse recopié dans un autre fichier) n'est pas vue ; l'heuristique `Bash` peut compter une commande qui ne fait que citer le fichier. ## Côté plugin Fichiers `plugins/miam/hooks/economies.ts` (calcul et format, testés) et `register.ts` (hooks : `tool.call` sur `deleguer` et `suivre` puis sur les reprises `Read`/`Edit`/`Write`/`NotebookEdit`/`Bash`, `turn.complete`, commande `/miam-economies`). L'envoi passe par `$.http.fetch` de Claude Code, sans modèle, donc sans token ; le jeton et le port viennent des réglages du plugin. Format et calcul : `economies.ts` fait foi (`EvenementDelegation`, `EvenementPilotage`, `EvenementCorrection`, `estimer`, `cout`, `cible`, `mesurerCorrection`) ; toute évolution du format change `SCHEMA` et ce document. Tests : `claude plugin test plugins/miam` (35 tests).