Le problème : un agent qui « a fini » sans appeler un outil ni rien produire a répondu OK sans rien faire, et personne ne s'en aperçoit — ni l'équipe, qui voit un journal vert, ni l'agent, qui a cru avoir fini. IA observe les agents qu'on déploie déjà : runs, étapes, jetons, coût, latence, traces imbriquées, jugements. Il fait attendre les actions sensibles qu'un humain les approuve. Et il rend l'état de l'agent à l'agent, ce qu'aucun outil d'observabilité ne fait.
Ce n'est pas un chatbot, pas un SDK, pas une instrumentation : des requêtes HTTP en JSON, que l'agent émet lui-même après coup.
Démarrer en 30 secondes
Rapporter un run après chaque exécution. L'agent existe dès son premier run — rien à déclarer avant.
```bash curl -X POST https://letock.fr/api/v1/agents/runs \ -H "Authorization: Bearer tock_…" -H "Content-Type: application/json" \ -d '{"agent":"assistant-commercial","resultat":"ok",
"resume":"Devis 4521 préparé","declencheur":"mail reçu",
"etapes":[{"outil":"gmail.lire","duree_ms":420,"ok":true},
{"outil":"crm.tarifs","duree_ms":80,"ok":true}],
"jetons":8200,"cout_centimes":3,"modele":"claude-sonnet-5"}'```
Réponse : 202 et {"enregistre":true,"run":"<uuid>","trace":"<uuid>","resultat":"ok","nouveau":true}.
resultat ne vaut que ok ou echec. Le troisième résultat, inerte, n'est jamais envoyé : Tock le déduit. Un run ok qui n'a appelé aucun outil en succès et n'a rien résumé est inerte, quoi qu'en dise l'agent.
Le vocabulaire
- Agent : un nom (
[a-z0-9._-], 60 caractères), un rôle facultatif, rattaché à un projet. Créé au premier run. - Run : une exécution. Déclencheur, étapes, résultat, résumé, jetons, coût en centimes, modèle, début et fin.
- Trace : un travail, et non des runs épars. Un run qui en déclenche un autre passe
parent; les deux portent alors le mêmetrace_id, et le coût de la demande s'additionne. - Variante : une étiquette libre posée sur le run —
prompt-v3,sans-outil-web. C'est elle qui rend deux versions comparables. - Note :
bonoumauvais, avec un motif. Posée par un humain sur l'écran (note_source: humain) ou par une évaluation automatique via l'API (auto). C'est la seule information que la plateforme ne peut pas produire seule. - Validation : une action sensible que l'agent dépose et qu'un humain approuve ou refuse. Sans réponse avant l'échéance, c'est un refus.
Les cinq signaux
Une fois par jour, sur le dernier jour complet, chaque agent est jugé contre lui-même : la valeur du jour contre la médiane des mêmes jours de semaine, cinq semaines en arrière. Rien ne se déclenche sous cinq runs dans la journée.
| signal | déclenchement |
|---|---|
inerte | la moitié des runs ont fini « OK » sans appeler d'outil ni rien produire |
echecs | plus d'un run sur quatre en échec |
cout | le coût du jour au double de la médiane des mêmes jours |
latence | le 95e centile de la durée au double de la médiane des mêmes jours |
qualite | plus d'un run jugé sur deux est noté mauvais (au moins trois jugements) |
Une alerte à l'ouverture d'un signal, une au retour à la normale, et rien entre les deux. Chaque ouverture dépose aussi un signal dans le journal de la plateforme, ce qui permet à Signaux de rapprocher la dérive d'un agent d'un déploiement ou d'une panne ailleurs.
La latence est jugée au 95e centile, pas à la moyenne : un agent qui répond en deux secondes sauf une fois sur dix où il met trois minutes a une moyenne rassurante et des utilisateurs qui abandonnent.
L'agent qui s'interroge lui-même
C'est ce qui n'existe nulle part ailleurs. L'agent appelle GET /api/v1/agents/etat au début de son run et reçoit son propre état, dont un champ conseils écrit pour être recopié tel quel dans le contexte du modèle.
``bash curl "https://letock.fr/api/v1/agents/etat?agent=assistant-commercial" \ -H "Authorization: Bearer tock_…" ``
```json { "agent": "assistant-commercial", "etat": "attention", "hier": { "jour": "2026-03-17", "runs": 10, "inertes": 6, "cout_centimes": 240, "duree_p95_ms": 9100 }, "sept_jours": { "runs": 61, "echecs": 2, "inertes": 9, "juges": 4, "mauvais": 3 }, "constats": [ { "signal": "inerte", "raison": "6 runs sur 10 ont fini « OK » …" } ], "alertes": ["inerte"], "validations_en_attente": [ { "id": "…", "action": "Envoyer le devis n° 4521", "expire_le": "…" } ], "runs_juges_mauvais": [ { "run": "…", "motif": "a répondu sur un autre dossier" } ], "quota": { "poste": "runsIa", "consomme": 612, "limite": 1000, "restant": 388, "part": 0.61 }, "conseils": [
"6 runs sur 10 ont fini « OK » sans appeler un outil ni rien produire. Avant de conclure, vérifie que tes outils ont répondu…", "1 action que tu as déposée attend encore un accord humain depuis 20 h. Ne la refais pas et ne la contourne pas : interroge GET /api/v1/agents/validations/…"
] } ```
Une clé en lecture seule suffit, et c'est la bonne façon de s'en servir : l'agent lit son état avec une clé qui ne peut rien écrire.
Ce que les conseils couvrent : chaque constat ouvert avec le geste correspondant, les validations en attente et leur ancienneté, les runs récemment jugés mauvais avec leurs motifs, et le quota du mois dès 70 % puis dès 90 %. Quand il n'y a rien à dire, ils le disent — une phrase, pas un silence qu'on interpréterait mal.
Les routes d'API
Authorization: Bearer tock_… partout. Les lectures exigent la mission « lire » ; l'écriture de runs et les demandes de validation exigent « rendre compte de ses propres exécutions » (ia.ecrire) ; trancher une validation exige « approuver des validations » (ia.valider), qui n'est jamais proposée à un agent. 300 appels par minute et par clé.
POST /api/v1/agents/runs — rapporter un run
| champ | type | notes | |
|---|---|---|---|
agent | texte | obligatoire | |
resultat | ok \ | echec | obligatoire |
resume | texte, 2 000 caractères | ce que l'agent a produit | |
declencheur | texte, 120 caractères | ce qui l'a lancé | |
etapes | tableau de {outil, duree_ms, ok, detail}, 200 entrées | ||
jetons, cout_centimes | entiers | le coût est en centimes, jamais en euros flottants | |
modele | texte | ||
debut, fin | dates ISO 8601 | défaut : maintenant | |
id | texte, 200 caractères | identifiant de l'appelant ; le même ne crée pas deux runs | |
projet | uuid | sinon le projet par défaut de l'équipe | |
parent | identifiant Tock ou identifiant de l'appelant du run appelant | forme la trace | |
variante | texte, 80 caractères | l'étiquette à comparer | |
note, note_motif | bon \ | mauvais, texte | verdict d'une évaluation automatique |
202 → { enregistre, run, trace, resultat, nouveau }. resultat peut valoir inerte alors que vous avez envoyé ok : c'est le jugement de Tock.
Un parent introuvable — purgé, ou d'une autre équipe — n'est pas une erreur : le run existe quand même, en tête de sa propre trace. Perdre le travail parce qu'on a perdu son contexte serait le pire des deux maux.
Erreurs : 400 champs_manquants, 400 refus_metier (nom d'agent invalide, début après la fin), 422 champ_note, 404 projet_inconnu, 429 quota_atteint.
GET /api/v1/agents/runs — chercher
Paramètres : projet, agent, resultat (ok, echec, inerte), variante, note (bon, mauvais, aucune), depuis et jusqua (dates ISO), limite (500 au plus), depuis_rang (décalage), format (json, csv, ndjson).
200 → { "runs": [ … ], "total": n, "projet": "<uuid>" }. Chaque run porte id, agent, declencheur, resultat, resume, outils, jetons, cout_centimes, duree_ms, modele, variante, note, note_source, note_motif, trace, parent, id_externe, debut, fin.
GET /api/v1/agents/runs/{id} — la trace
On peut désigner n'importe quel maillon — souvent celui qui a échoué — et on obtient l'arbre entier.
200 → { trace, runs, agents, cout_centimes, jetons, duree_ms, echecs, arbre }. arbre est une liste de racines, chaque nœud portant ses appels (ses enfants). duree_ms est le temps vécu, du début du premier run à la fin du dernier, pas la somme des durées. Au-delà de 500 runs, la trace est coupée.
PATCH /api/v1/agents/runs/{id} — juger un run
``bash curl -X PATCH https://letock.fr/api/v1/agents/runs/ID -H "Authorization: Bearer tock_…" \ -H "Content-Type: application/json" -d '{"note":"mauvais","motif":"a répondu sur un autre dossier"}' ``
note vaut bon, mauvais, ou null pour retirer le jugement. Posée par une clé d'API, la note est marquée auto ; posée sur l'écran, humain. 200 → { run, note }.
POST /api/v1/agents/validations — demander un accord
| champ | type | notes |
|---|---|---|
agent | texte | obligatoire |
action | texte, 3 à 300 caractères | ce que l'agent veut faire, en une phrase |
detail | texte, 4 000 caractères | |
delai_h | entier, 1 à 168 | défaut 48 |
webhook | URL https | reçoit la décision |
run | uuid | le run qui demande |
projet | uuid |
202 → { validation, etat, expire_le }. L'équipe reçoit un courrier avec un lien vers l'écran. Erreurs : 400 champs_manquants, 400 refus_metier (action trop courte, webhook non https), 404 projet_inconnu.
GET /api/v1/agents/validations/{id} — interroger la décision
200 → { validation, agent, action, etat, reponse, cree_le, decide_le, expire_le }. etat vaut en_attente, approuvee, refusee ou expiree. C'est l'appel que fait l'agent en boucle lente, ou qu'il évite en donnant un webhook.
PATCH /api/v1/agents/validations/{id} — trancher depuis ailleurs
Corps { "decision": "approuvee" | "refusee", "reponse": "…" }. Réservé à une intégration que vous écrivez — un bouton dans un Slack — et protégé par la mission « approuver des validations », qui n'est jamais proposée à un agent : une validation humaine que l'agent peut s'accorder lui-même n'est plus une validation.
GET /api/v1/agents/etat — l'état rendu à l'agent
Paramètres : agent (obligatoire), projet. Réponse décrite plus haut. 404 agent_inconnu tant qu'aucun run n'a été rapporté sous ce nom.
Le webhook des validations
Si webhook est donné, Tock appelle cette adresse en POST dès que la validation est tranchée ou expirée, avec { "validation": "<uuid>", "etat": "…", "reponse": "…", "action": "…" }.
Un seul appel, jamais deux : la validation est marquée comme notifiée avant l'appel. Il n'y a donc pas de réessai — un webhook injoignable au mauvais moment perd la notification, et l'agent doit retomber sur l'interrogation. C'est assumé : rappeler le webhook d'un client toutes les minutes parce qu'il est mort coûte plus cher que l'interrogation qu'il remplace. L'adresse est vérifiée avant l'appel (pas d'IP privée, pas de redirection vers le réseau interne).
Les écrans
`/ia` — cinq sections, chacune à son adresse.
- *Agents* (défaut) : les actions en attente d'accord en tête, puis chaque agent avec sa semaine, ses trente jours en barres, ses constats, ses huit derniers runs et les deux boutons de jugement.
- *Traces* : les dernières traces à plusieurs runs, avec le coût et la durée de la demande entière.
- *Coûts* : le mois en cours, la projection de fin de mois au rythme actuel, le mois précédent, le coût par jour sur trente jours, puis le détail par modèle (coût par run, jetons, durée, échecs) et par agent.
- *Comparer* : pour chaque agent qui a plusieurs variantes, le tableau des mesures et une comparaison écrite qui dit quand elle ne conclut pas.
- *Validations* : ce qui attend, et l'historique des décisions.
`/ia/{run}` — la trace d'un run : l'arbre des délégations, les étapes de chaque nœud, et le formulaire de jugement avec motif.
Comparer deux variantes
Le tableau donne runs, échecs, inertes, coût moyen par run, médiane, 95e centile et part de runs jugés bons. La comparaison écrite applique un test de proportions à deux échantillons : au-delà de deux écarts-types, l'écart est dit « net » ; en deçà, il est dit « peut être le hasard ». En dessous de vingt runs de chaque côté, aucun écart n'est qualifié — les chiffres sont affichés, avec la phrase qui dit de ne pas les croire.
Il n'y a volontairement aucun score global. Additionner un coût, une latence et un taux d'échec dans un seul chiffre revient à choisir à la place de l'équipe, avec des pondérations qu'on ne lui a jamais montrées.
Quotas
Un run rapporté consomme une unité du poste `runsIa` du forfait de l'équipe (valeurs dans src/plateforme/forfaits.ts, visibles sur « Mon compte »). Au-delà : 429 quota_atteint, et le run n'est pas enregistré. Un run refusé n'est pas compté.
Les validations, les jugements, les traces et les lectures ne consomment rien de ce poste. Les courriers d'alerte consomment le poste alertes, commun à toute la plateforme.
Les pièges
- `inerte` n'est pas une valeur qu'on envoie, c'est un verdict. Un
oksans outil en succès et sans résumé devientinerte. Envoyer un résumé vide ne trompe personne ; envoyer un résumé honnête est le bon geste. - Un run reste isolé si `parent` manque. La trace ne se devine pas : sans le champ, deux runs liés restent deux lignes, et le coût de la demande ne s'additionne jamais.
- `id` (l'identifiant de l'appelant) dédoublonne, il ne met pas à jour. Renvoyer le même
idavec d'autres chiffres ne corrige rien : le premier run fait foi. - Le coût est en centimes entiers. Un run à 0,004 € rapporté comme
cout_centimes: 0disparaît des totaux. Agréger avant de rapporter, ou accepter l'arrondi. - Une note posée par une clé d'API est marquée `auto`. Un agent peut donc se noter lui-même, et le signal
qualitecompte ces notes. Si le jugement doit peser, qu'il vienne d'un humain sur l'écran, ou d'une évaluation indépendante de l'agent jugé. - Les runs sont purgés à quatre-vingt-dix jours. Les traces, les variantes et les jugements disparaissent avec eux ; les comparaisons portent sur trente jours.
- La veille ne juge que le dernier jour complet, une fois par jour. Un agent qui déraille ce matin sera jugé demain. Ce qui est immédiat, c'est l'écran — et
/api/v1/agents/etat, qui lit les mêmes séries à la demande. - Sans réponse, une validation est refusée. Jamais approuvée par défaut, jamais prolongée en silence.
- La projection de fin de mois est une règle de trois : dépense du mois rapportée aux jours écoulés. Elle ne connaît ni vos jours fériés ni votre campagne de la semaine prochaine. Elle est là pour faire réagir, pas pour budgéter.