# Troncature des résultats d’API : empêchez les agents d’effectuer de mauvaises modifications

Un agent n’a pas besoin d’un outil malveillant pour effectuer une modification dommageable. Il suffit qu’un outil ne renvoie discrètement qu’une partie de sa réponse. Donnez à un agent un résultat de recherche qui semble complet, demandez-lui de supprimer ce que la recherche n’a pas trouvé, et il produira souvent une erreur parfaitement logique fondée sur des prémisses fausses.

La solution n’est pas d’ajouter au prompt système une phrase demandant à l’agent d’être prudent. L’outil doit indiquer, sous une forme stable et lisible par la machine, s’il a terminé le travail demandé, ce qu’il a omis, pourquoi il l’a omis et comment l’appelant peut poursuivre. Si l’outil ne peut pas fournir ces informations, l’agent ne doit pas interpréter l’absence d’un résultat comme une autorisation de procéder à une modification globale.

## Les données partielles et les données vides ne sont pas la même affirmation

Un résultat vide signifie que l’outil n’a trouvé aucun élément correspondant dans le périmètre qu’il a réellement examiné. Un résultat vide complet signifie que l’outil a examiné l’intégralité du périmètre demandé et n’a rien trouvé. Ces deux affirmations sont différentes, pourtant la plupart des contrats d’API les réduisent au même `[]`.

Cette confusion entraîne une déduction erronée bien précise :

1. L’agent demande tous les comptes de service sans responsable actuel.
2. L’API renvoie un tableau vide après avoir parcouru sa première page, atteint une limite de résultats ou omis des enregistrements que le jeton ne permet pas de lire.
3. L’agent en conclut que chaque compte possède un responsable.
4. Il modifie un contrôle, un rapport ou une tâche de nettoyage connexe sur la base de cette conclusion.

L’agent n’a pas eu besoin de mal comprendre la langue. L’outil lui a fourni une réponse dont la forme suggérait plus que ce que le serveur savait réellement.

Un outil doit distinguer au moins quatre états. Une requête complète peut renvoyer des éléments. Une requête complète peut ne rien renvoyer. Une requête incomplète peut renvoyer quelques éléments. Une requête incomplète peut ne rien renvoyer. Ne considérer comme important que le troisième état fait oublier le cas le plus dangereux : une réponse vide qui persuade l’agent qu’un problème n’existe pas.

Les autorisations aggravent encore la situation. De nombreux services masquent volontairement les objets inaccessibles en renvoyant une collection vide ou filtrée plutôt qu’une erreur d’autorisation. Ce comportement peut être raisonnable dans une interface destinée aux humains. Il ne constitue pas une preuve acceptable pour une tâche de nettoyage autonome, sauf si l’API indique exactement ce que l’appelant pouvait voir.

N’utilisez pas une phrase comme `Some results may be missing` comme contrat. Elle ne fournit à l’agent aucune branche fiable à suivre et ne donne à l’ingénieur aucune condition testable. Un champ nommé `complete` avec une valeur booléenne peut sembler banal. C’est précisément l’objectif.

## Une réponse HTTP réussie peut tout de même être incomplète

Les codes d’état HTTP décrivent l’échange entre le client et le serveur. Ils ne prouvent pas à eux seuls qu’une recherche, un inventaire ou une exportation couvre le domaine demandé.

RFC 9110 définit la sémantique des codes d’état HTTP. `200 OK` indique que la requête a réussi selon la sémantique de la méthode. Il ne signifie pas qu’une recherche a couvert chaque page, chaque partition, chaque domaine d’autorisation ou chaque enregistrement avant une échéance. Les équipes donnent souvent à `200` une portée que le protocole ne lui attribue pas.

Prenons cette réponse :

```json
HTTP/1.1 200 OK
Content-Type: application/json

{
  "items": [],
  "next_cursor": null
}
```

Elle semble définitive. Pourtant, `next_cursor: null` signifie seulement que ce mécanisme de pagination précis ne possède pas de page suivante. Cela ne dit rien d’une limite de résultats du backend, d’une tâche de recherche expirée, d’une source en échec, d’enregistrements exclus par une règle ou d’une API qui limite silencieusement la période consultée.

Une réponse devient une preuve exploitable lorsque le contrat précise ce que couvre `complete`. Pour une recherche de comptes, cela peut signifier tous les comptes visibles par le principal appelant dans un instantané donné. Pour une recherche de code, cela peut signifier tous les fichiers indexés dans une révision donnée, à l’exclusion explicite des fichiers ignorés et des sorties générées non indexées. Le périmètre doit être assez concret pour que l’appelant puisse décider s’il correspond à l’action envisagée.

Ne résolvez pas ce problème en renvoyant `500` pour chaque réponse partielle. Les résultats partiels peuvent être utiles. Un tableau de bord peut les afficher. Un agent peut les résumer. Un humain peut les examiner. L’erreur consiste à présenter un résultat partiel comme une réponse faisant autorité à une question qui exige l’exhaustivité.

Utilisez une erreur lorsque l’opération demandée promet une réponse atomique ou complète et ne peut pas tenir cette promesse. Utilisez une réponse réussie indiquant explicitement son caractère incomplet lorsque les données partielles ont une utilité légitime. Le client a besoin d’une distinction déterministe, pas d’un débat sur le caractère trop optimiste de `200`.

## Placez les métadonnées d’exhaustivité à côté de chaque résultat

Un contrat de résultat doit exposer l’exhaustivité sous forme de données structurées, que la liste soit complète, courte ou vide. Ne forcez pas les appelants à la déduire du nombre d’éléments, d’un en-tête absent ou d’une phrase dans un champ `message`.

Cette structure convient à une recherche de collection :

```json
{
  "items": [
    {"id": "svc-184", "owner": null}
  ],
  "complete": false,
  "truncated": true,
  "incomplete_reasons": [
    {
      "code": "RESULT_LIMIT_REACHED",
      "message": "The query stopped after the configured result limit.",
      "limit": 1000
    }
  ],
  "next_cursor": "eyJvZmZzZXQiOjEwMDB9",
  "scope": {
    "resource": "service_accounts",
    "visibility": "resources readable by this credential",
    "snapshot": "2025-03-08T14:20:11Z"
  },
  "warnings": []
}
```

Les noms exacts des champs comptent moins que leur signification et leur cohérence. `complete` est le champ de décision. `truncated` décrit une cause importante d’incomplétude, mais ne doit pas devenir un fourre-tout. Un filtre d’autorisation n’est pas une troncature. Une recherche fédérée interrompue par un délai n’est pas un problème de pagination. Si vous surchargez un seul indicateur, les appelants perdent la raison dont ils ont besoin pour récupérer les données en toute sécurité.

Gardez `warnings` séparé de `incomplete_reasons`. Un avertissement peut signaler qu’un champ obsolète est apparu, qu’une valeur a été normalisée ou qu’un tri demandé a été remplacé par un tri par défaut. Une raison d’incomplétude indique que la réponse ne peut pas soutenir d’affirmation sur la partie non renvoyée du périmètre demandé. Cette distinction détermine si un agent peut poursuivre.

Évitez également un simple indicateur `has_more` comme unique signal. Il répond généralement à une question de pagination limitée. Un agent qui voit `has_more: false` peut raisonnablement conclure que la collection est terminée, même si une limite côté serveur ou un fragment inaccessible a empêché une exploration complète. `has_more` peut rester présent, mais il ne doit pas porter seul toute la responsabilité de l’exhaustivité.

Pour la lecture d’une ressource unique, appliquez la même discipline. Une réponse dont certains champs sont omis doit préciser si le serveur les a omis parce que l’appelant ne les a pas demandés, parce qu’il n’y a pas accès, parce que la source de données a échoué ou parce que la valeur est réellement absente. L’omission JSON est compacte, mais ambiguë.

## La pagination a besoin d’une limite stable, pas d’une page plus grande

La pagination n’est sûre pour les agents que si l’API rend la poursuite fiable et explique quelles modifications peuvent l’invalider. Augmenter la limite de page ne fait que repousser le problème.

La pagination par décalage favorise particulièrement les mauvaises conclusions. Un agent lit les enregistrements 0 à 99, supprime ou crée un objet, puis lit les enregistrements 100 à 199. Si l’ordre sous-jacent a changé, il peut ignorer un enregistrement ou en traiter un deux fois. Pour un rapport informatif, cela peut être acceptable. Pour un plan de modification, les conséquences peuvent être graves.

La pagination par curseur est généralement préférable, car le serveur peut encoder une position dans un ensemble de résultats ordonné. Elle a tout de même besoin d’un contrat. Indiquez si le curseur fige un instantané, combien de temps il reste valide et si la modification des filtres, du tri ou des autorisations l’invalide. Si un curseur expire, ne redémarrez pas discrètement l’exploration pour renvoyer une réponse fusionnée. Renvoyez un état explicitement incomplet ou forcez le client à recommencer.

Une réponse de collection utile fournit à l’appelant assez d’informations pour terminer volontairement :

```json
{
  "items": ["item-001", "item-002"],
  "complete": false,
  "next_cursor": "cD0y",
  "page": {
    "returned": 2,
    "requested_size": 2,
    "ordering": "id ascending",
    "snapshot": "search-7f9c"
  },
  "incomplete_reasons": [
    {"code": "MORE_PAGES_AVAILABLE"}
  ]
}
```

L’appelant doit poursuivre jusqu’à recevoir `complete: true`, et non simplement jusqu’à obtenir une page courte. Les pages courtes peuvent avoir de nombreuses causes. Certaines API les renvoient parce qu’une partition est temporairement peu remplie, qu’un processus interne s’est arrêté prématurément ou que le service limite la réponse en octets plutôt qu’en nombre d’objets.

Ne demandez pas au modèle de langage de mémoriser cette boucle dans un texte. Intégrez le comportement de pagination dans l’outil. Un outil de haut niveau comme `search_all` peut collecter les pages, conserver l’instantané, limiter son propre travail et indiquer s’il a atteint un état terminal. S’il atteint sa propre limite, il doit renvoyer `complete: false` et préciser que cette limite côté client en est la cause.

Ce dernier cas est souvent oublié. Les ingénieurs ajoutent correctement des métadonnées à l’API, puis construisent un wrapper d’agent avec `max_pages=10` et abandonnent l’information indiquant qu’il s’est arrêté à dix pages. Le wrapper devient alors la source de l’incomplétude. Le contrat de l’outil le plus extérieur doit divulguer cette information.

## Les délais, fragments défaillants et autorisations ont besoin de raisons distinctes

Une recherche peut terminer sa requête HTTP alors qu’une partie de son travail n’est pas terminée. Les services distribués répartissent souvent une requête entre plusieurs index ou locataires. Si une source dépasse le délai et que le service renvoie les correspondances des autres sources, le résultat peut être utile, mais il reste incomplet.

Représentez la cause par un code permettant au programme de choisir une branche. Le texte destiné aux humains doit l’accompagner, jamais le remplacer. Gardez les codes peu nombreux, stables et documentés. Par exemple :

- `MORE_PAGES_AVAILABLE` signifie que l’appelant peut demander la page suivante.
- `RESULT_LIMIT_REACHED` signifie que le service a appliqué une limite avant d’épuiser les correspondances.
- `TIME_BUDGET_EXCEEDED` signifie que la recherche s’est arrêtée avant la fin du travail prévu.
- `SOURCE_UNAVAILABLE` signifie qu’une source identifiée n’a pas répondu.
- `VISIBILITY_RESTRICTED` signifie que les autorisations de l’appelant excluaient une partie du domaine demandé.

Ne dissimulez pas `VISIBILITY_RESTRICTED` derrière une réponse générique indiquant un succès. Les équipes de sécurité préfèrent parfois des réponses impossibles à distinguer, afin de ne pas révéler quels objets existent. Cette préoccupation est légitime. L’API peut signaler que les limites de visibilité empêchent un inventaire complet sans nommer les objets cachés. Elle ne doit pas laisser l’appelant confondre un inventaire partiel avec un inventaire exhaustif.

La même règle s’applique aux limites de débit et aux quotas. Si une API lit la première partie d’une requête avant d’épuiser un budget, elle doit renvoyer les données obtenues et la condition liée au budget. Une nouvelle tentative pourra peut-être terminer le travail, mais elle constitue une nouvelle tentative. L’agent ne doit pas combiner deux tentatives pour affirmer que le résultat est complet, sauf si l’API lui fournit un instantané stable ou si la tâche tolère les changements.

Une échéance doit être une entrée aussi bien qu’une sortie. Lorsqu’un agent demande un inventaire étendu, laissez-le fixer un budget de temps et recevoir la quantité de travail accomplie. Le compromis devient ainsi visible. Une recherche exploratoire de dix secondes peut convenir avant un examen humain. Elle constitue une preuve fragile pour supprimer chaque ressource que la recherche n’a pas vue.

## L’absence est une preuve faible pour les modifications destructrices

Un agent peut utiliser des données partielles pour préparer un rapport, identifier des candidats ou demander à un humain d’examiner une petite cible. Il ne doit pas s’en servir pour conclure qu’une ressource est inutilisée, sans responsable, en double ou sûre à supprimer.

La différence tient au sens de l’affirmation. Trouver un enregistrement avec `owner: null` constitue une preuve positive concernant cet enregistrement, sous réserve de la fraîcheur du champ. Ne trouver aucun enregistrement sans responsable est une affirmation universelle sur le domaine de recherche. Les affirmations universelles exigent une couverture complète d’un périmètre défini.

Cet échec se présente souvent comme une amélioration de l’efficacité. Une équipe fournit à un agent un outil nommé `list_inactive_projects`, puis l’autorise à archiver chaque projet renvoyé ou, pire encore, chaque projet absent d’une seconde liste. L’outil possède un nombre maximal de résultats. Une grande organisation dépasse cette limite quelques mois plus tard. Personne ne modifie le prompt de l’agent, mais sa mission passe de « travailler sur l’inventaire » à « travailler sur un préfixe arbitraire de l’inventaire ».

Concevez les outils d’action pour qu’ils exigent des preuves plutôt qu’un récit. Une opération d’archivage peut demander les identifiants sélectionnés par un inventaire complet précédent et un jeton d’instantané qui lie la sélection à la lecture. Si l’inventaire était incomplet, l’outil refuse l’opération. Le contrôle de sécurité se trouve alors à un endroit où un modèle ne peut pas l’écarter d’un geste.

Pour les actions qui ne peuvent pas utiliser de jeton d’instantané, exigez un périmètre explicite et revérifiez chaque cible au moment de l’exécution. Cela ne prouve pas que la recherche initiale était exhaustive, mais empêche une liste obsolète d’autoriser des mutations sans rapport. Gardez l’action suffisamment limitée pour qu’un réviseur puisse comprendre l’ensemble des cibles.

L’alternative courante consiste à dire à l’agent : « Ne supprimez rien sauf si vous êtes certain. » Cela semble raisonnable et échoue en pratique. La certitude est un mot dans un prompt. `complete: false` est une condition qu’un outil peut faire respecter.

## Les schémas d’outils doivent forcer l’agent à regarder l’incertitude

Un outil MCP, ou tout wrapper destiné à un agent, doit renvoyer une enveloppe typée plutôt qu’un bloc de texte séduisant. Le modèle peut lire du texte, mais les logiciels environnants ont besoin de champs qu’ils peuvent valider, journaliser, bloquer et tester.

Un type de réponse pratique pourrait ressembler à ceci :

```json
{
  "status": "partial",
  "data": {
    "repositories": [
      {"id": "repo-a", "default_branch": "main"}
    ]
  },
  "completeness": {
    "complete": false,
    "reasons": ["TIME_BUDGET_EXCEEDED"],
    "continuation": {
      "kind": "retry_with_deadline",
      "minimum_seconds": 30
    }
  },
  "warnings": [
    {
      "code": "STALE_INDEX",
      "message": "Search index may lag the source repository."
    }
  ]
}
```

N’utilisez pas `status: "success"` pour cette réponse. Les clients simples risqueraient d’abandonner les métadonnées. `partial` indique que l’appelant a reçu des données utilisables assorties d’une limite. Si votre protocole doit utiliser un seul état de succès, rendez `complete` obligatoire et exigez des clients capables d’agir qu’ils l’examinent avant toute mutation.

Le champ de continuation doit décrire une véritable voie de récupération. `next_cursor` convient à une page supplémentaire. `retry_after` convient à une limite de débit. `narrow_query` peut convenir à une limite du serveur. Ne fournissez pas une continuation qui répète simplement la même requête en espérant que le contexte ait changé.

Les instructions de l’agent doivent établir un petit ensemble de règles strictes :

- L’agent peut citer une collection vide comme preuve d’absence uniquement lorsque `complete` vaut `true`.
- L’agent peut utiliser une réponse partielle pour proposer une investigation en lecture seule et limitée.
- L’agent doit présenter les `incomplete_reasons` avant de demander une approbation pour toute action fondée sur le résultat.
- L’agent ne doit pas inventer un jeton de continuation manquant ni prétendre qu’une nouvelle tentative a réussi sans en avoir reçu le résultat.

Ces règles sont courtes parce que les données portent les détails. Un prompt ne peut pas récupérer une information que l’outil a choisi de ne pas signaler.

## Les avertissements ont besoin d’un responsable et d’une voie d’expiration

Les avertissements deviennent du papier peint lorsque chaque réponse émet une mise en garde vague. Gardez-les précis, attribuables et exploitables. Un avertissement qui ne change jamais le choix suivant de l’appelant devrait généralement devenir de la documentation ou disparaître.

Par exemple, `STALE_INDEX` devrait identifier la source indexée et, lorsque c’est possible, sa révision observée ou son heure de mise à jour. L’agent peut alors décider d’inspecter la source de référence avant de modifier le code. `PARTIAL_FIELD_SET` devrait indiquer quels champs le serveur a omis et si l’appelant peut les demander. `DEFAULT_SCOPE_APPLIED` devrait préciser le périmètre choisi par le serveur, car les valeurs par défaut provoquent souvent des actions globales involontaires.

Ne transformez pas accidentellement les avertissements en blocages. L’appelant a besoin d’une règle claire de gravité. Les métadonnées d’exhaustivité déterminent si le résultat permet une affirmation sur l’ensemble du périmètre. Les avertissements concernent la confiance, la fraîcheur ou l’interprétation. Un outil peut renvoyer `complete: true` avec un avertissement de vétusté. Le résultat peut tout de même énumérer chaque élément d’un index tout en étant inadapté à une modification exigeant un état en temps réel.

Attribuez aux avertissements des codes stables et testez les consommateurs avec ces codes. Évitez les tests qui vérifient uniquement un message agréable. Les messages évoluent lorsqu’un rédacteur améliore la formulation ; la règle de décision, elle, ne devrait pas changer.

Décidez aussi à qui revient la responsabilité d’un avertissement après sa mise en production. Si une équipe opérationnelle voit le même avertissement à chaque appel pendant six mois, elle cessera de le lire. Réparez la cause, transformez l’avertissement en échec bloquant lorsque c’est nécessaire ou supprimez-le s’il n’influence aucune décision. Des voyants jaunes permanents apprennent aux humains comme aux agents à ignorer les voyants jaunes.

## Les tests doivent couvrir la réponse vide dangereuse

La plupart des suites de tests couvrent une page normale de résultats et une erreur serveur. Elles oublient la réponse qui provoque la pire déduction : `items: []` accompagné d’une incomplétude.

Écrivez des tests de contrat pour chaque code de raison. Vérifiez que l’API renvoie les métadonnées pour les listes remplies comme pour les listes vides, que les SDK les conservent et que le wrapper de l’agent ne les aplatisse pas en texte. Une régression à n’importe quel niveau peut transformer une réponse honnête du serveur en résultat trompeur pour l’outil.

Utilisez des cas comme celui-ci dans une fixture de test :

```json
{
  "case": "empty first page with more pages",
  "response": {
    "items": [],
    "complete": false,
    "truncated": false,
    "incomplete_reasons": ["MORE_PAGES_AVAILABLE"],
    "next_cursor": "cursor-2"
  },
  "expected_agent_decision": "continue_search"
}
```

Testez ensuite une demande de mutation après cette fixture. La décision attendue doit être `refuse_or_request_review`, et non `perform_cleanup`. Rendez la règle visible dans le nom du test. Les futurs mainteneurs risqueraient sinon de considérer cette protection comme un cas limite trop prudent et de la supprimer pour rendre une démonstration d’automatisation plus fluide.

Testez aussi la pagination pendant les mutations. Insérez, supprimez et réordonnez des enregistrements entre les pages. Faites expirer un curseur. Faites échouer un fragment après que d’autres ont renvoyé des résultats. Retirez une autorisation au milieu d’une exploration. Votre outil doit soit conserver un instantané documenté, soit signaler qu’il ne peut pas garantir l’exhaustivité. Un test qui utilise seulement une base de données fictive statique ne peut pas détecter les mensonges qui apparaissent en production.

Les tests basés sur les propriétés sont utiles ici. Générez des collections plus grandes que chaque limite configurée, faites varier les tailles de page et vérifiez un invariant : un client ne peut déclarer une collection complète qu’après avoir comptabilisé chaque élément de l’instantané déclaré. Le test n’a pas besoin de modèle de langage. Il s’agit d’une vérification ordinaire de l’interface.

## L’approbation humaine doit montrer les preuves manquantes

Le contrôle humain ne fonctionne que si l’approbation montre la décision que l’on demande réellement à une personne de prendre. « Autoriser l’action de l’agent » n’est pas une approbation. C’est une demande d’accepter une chaîne opaque d’hypothèses.

Lorsqu’un outil signale des données incomplètes, montrez l’action proposée, le périmètre ciblé, la raison de l’incomplétude des preuves et la possibilité de récupération. Une demande utile indique que l’inventaire a dépassé le délai après avoir renvoyé 842 ressources et demande s’il faut réessayer avec une échéance plus longue, limiter l’action aux identifiants renvoyés ou abandonner la modification. Le réviseur peut alors faire un véritable choix.

Sallyport garde les identifiants hors du processus de l’agent lorsqu’il effectue des actions HTTP et SSH, et ses journaux d’activité peuvent afficher les appels effectués. Cette isolation et cette traçabilité sont utiles lorsqu’un réviseur doit reconstituer une mauvaise décision. Elles ne transforment pas une réponse d’API ambiguë en preuve ; la réponse de l’outil doit donc toujours porter son état d’exhaustivité.

Évitez la fatigue liée aux approbations en les réservant aux ambiguïtés importantes. Un outil doit gérer sans interrompre constamment une personne la poursuite courante, comme la récupération d’une page suivante documentée. Il doit s’arrêter lorsqu’il atteint une limite de politique : instantané expiré, visibilité restreinte, action fondée sur une absence ou mutation proposée au-delà des preuves collectées.

La première tâche d’ingénierie est modeste : trouvez chaque wrapper d’API capable de renvoyer une liste, un agrégat ou un résultat de recherche, puis ajoutez un état `complete` explicite à sa réponse extérieure. Commencez par les résultats vides et les recherches limitées. C’est là que les agents sûrs d’eux fabriquent les réponses erronées les plus nettes.
