# Pagination API pour les agents IA : une découverte bornée

Un agent qui appelle un endpoint de liste sans budget de pages ne fait pas de découverte. Il lance une procédure distante sans fin précise et espère que la facture, la limite de débit et le volume de résultats resteront raisonnables. L'erreur inverse est tout aussi problématique : récupérer la première page, voir une réponse plausible et la traiter discrètement comme l'ensemble du système.

La pagination change ce qu'un agent peut affirmer honnêtement. Un élément renvoyé prouve que cet élément existe. Il ne prouve pas qu'il n'y en a pas d'autres. L'absence d'un élément ne prouve presque rien, sauf si l'agent peut montrer le périmètre inspecté, l'ordre utilisé et la raison pour laquelle le serveur a signalé la fin.

J'ai vu des agents transformer une demande anodine comme « trouver les jetons d'accès obsolètes » en milliers d'appels parce que personne ne leur avait indiqué où s'arrêtait la découverte. J'ai aussi vu un inventaire limité à une page conduire à une tentative de nettoyage de comptes présents sur la deuxième page. La solution ne consiste pas à rédiger une invite plus habile. Il faut fournir à l'agent un contrat de parcours borné et faire apparaître cette limite dans son rapport final.

## Les appels de découverte ont besoin d'un budget explicite

Tout appel de découverte paginé doit avoir des limites que l'agent ne peut pas étendre en silence. Définissez avant la première requête un nombre maximal de pages, un nombre maximal d'enregistrements renvoyés, une échéance et une marge pour la limite de débit. Les valeurs adaptées dépendent de la tâche, mais la présence de limites ne dépend pas de la tâche.

Traitez la découverte comme une phase distincte de l'action. Pendant la découverte, l'agent rassemble des identifiants et des faits. Il ne doit pas supprimer, renouveler ou modifier des objets simplement parce qu'une page contient quelque chose de suspect. Une fois les éléments suffisants réunis, il peut présenter un plan défini ou commencer une phase d'action autorisée séparément.

Un contrat utile comporte quatre éléments :

- Le endpoint et tous les filtres, y compris l'ordre de tri lorsque l'API en propose un.
- Une taille de page et un plafond de pages ou d'enregistrements.
- Une condition de fin définie par l'API, par exemple l'absence d'un curseur suivant.
- Une condition d'arrêt anticipé, comme la découverte d'un objet nommé ou l'épuisement du budget prévu.

Ne confondez pas plafond d'enregistrements et plafond de pages. Si le service autorise 100 enregistrements par page et que l'agent est limité à 500 enregistrements, cinq appels peuvent suffire. Si le service réduit la taille effective des pages à cause des filtres ou des permissions, la même tâche peut demander davantage d'appels. Une bonne implémentation vérifie les deux limites après chaque réponse.

Par exemple, un agent chargé de localiser un dépôt nommé `billing-service` peut s'arrêter dès qu'il reçoit une correspondance exacte si le filtre et l'ordre du endpoint rendent cette conclusion sûre. Un agent chargé d'identifier tous les dépôts sans protection des branches ne peut pas s'arrêter à la première correspondance. Cette tâche exige une énumération complète ou un résultat explicitement partiel.

Le mot « tous » doit avoir un coût. Un agent ne peut l'employer qu'après avoir atteint la condition finale du serveur sans dépasser son budget de pages, d'enregistrements, de temps ou d'erreurs. Si l'une de ces limites est atteinte, le rapport doit dire « scan partiel » et préciser la limite concernée.

## La taille de page contrôle le coût, pas l'exhaustivité

Le paramètre `limit`, `per_page` ou `page_size` indique au service combien d'enregistrements il doit essayer de renvoyer dans une réponse. Il ne dit pas au service quelle portion de la collection l'agent doit inspecter. Le régler au maximum réduit certains allers-retours, mais peut rendre chaque réponse assez coûteuse pour provoquer un délai d'attente, dépasser le budget de contexte ou noyer des détails importants dans une masse de données inutiles.

Commencez par la plus petite page qui permet de prendre la décision. Si l'agent doit trouver un compte précis, demander 20 enregistrements succincts vaut généralement mieux que demander 1 000 objets complets. S'il doit constituer un inventaire, utilisez une taille plus importante uniquement après avoir vérifié que le endpoint renvoie une représentation utile et bornée.

Les champs comptent autant que la taille des pages. De nombreuses API proposent `fields`, `include`, `expand` ou un mécanisme similaire. Une passe de découverte doit demander les identifiants, les noms, l'état, les horodatages et la propriété qui guide la décision. Ne récupérez les détails complets que pour les candidats qui doivent être examinés. Vous réduirez ainsi le trafic et donnerez au modèle moins de texte accessoire à mal interpréter.

Utilisez une forme de requête comme celle-ci lorsque le fournisseur prend en charge la pagination par curseur :

```http
GET /v1/projects?state=active\u0026limit=50\u0026sort=id HTTP/1.1
Authorization: Bearer injected-by-gateway
Accept: application/json
```

Attendez-vous à une réponse qui sépare les enregistrements de l'état de continuation :

```json
{
  "data": [
    {"id": "prj_104", "name": "billing-service", "state": "active"}
  ],
  "next_cursor": "eyJvcmRlciI6ImlkIiwicG9zIjoiMTA0In0"
}
```

L'agent doit noter qu'il a demandé 50 enregistrements et en a reçu un. Il ne doit pas déduire que la collection est terminée à partir de la brièveté du tableau. Dans cet exemple, le seul signal de fin utile est l'absence de `next_cursor` ou sa valeur null documentée.

Évitez une règle arbitraire comme « utilisez toujours 100 ». La taille maximale varie selon les API et certains services comptent les objets enfants développés ou les octets de réponse dans des limites différentes. Ne demandez la taille maximale documentée que lorsque la tâche en profite. Pour les scans larges, une taille moyenne produit souvent de meilleurs points de contrôle, des reprises plus faciles et des rapports qu'un humain peut auditer.

## Conservez les curseurs comme un état opaque du serveur

Un curseur n'est pas un offset auquel on aurait donné un nom sophistiqué. Le serveur en détient la signification et l'agent doit le recopier octet par octet dans la requête suivante. Il peut contenir une position de tri, un identifiant d'instantané, une limite de permission ou une signature. Le fait qu'il ressemble à du Base64 n'autorise pas à le décoder, le modifier ou en fabriquer un.

La boucle sûre est simple : demander la première page avec des filtres et des paramètres de tri stables, enregistrer le curseur renvoyé, puis soumettre la même requête avec ce curseur. Conservez tous les paramètres d'origine, sauf si la documentation de l'API indique explicitement le contraire. Modifier le filtre entre deux requêtes peut invalider le curseur ou, pire, produire un résultat plausible mais discontinu.

```text
request = { state: "active", limit: 50, sort: "id" }
seen_ids = set()
pages = 0

while pages \u003c 10 and len(seen_ids) \u003c 500:
    response = GET /v1/projects with request
    record response status, request, and response cursor

    for item in response.data:
        if item.id in seen_ids:
            report "duplicate record encountered" with item.id
            stop or apply the provider's documented recovery method
        seen_ids.add(item.id)

    pages += 1
    if response.next_cursor is absent:
        report "complete"
        break

    request.cursor = response.next_cursor
else:
    report "partial: traversal budget reached"
```

La détection des doublons n'est pas décorative. Les collections modifiables peuvent bouger pendant le parcours et les implémentations de pagination défectueuses existent. Un doublon ne signifie pas toujours que le service a échoué, mais il signifie que l'agent ne doit plus prétendre disposer d'une énumération propre. Si le fournisseur propose un jeton d'instantané, un paramètre `as_of` ou un mode de cohérence documenté, utilisez-le pour les tâches qui conduiront à une action importante.

L'expiration des curseurs doit avoir une règle explicite. Certains services ont des curseurs à courte durée de vie ; d'autres les lient à une session ou les invalident lorsque la requête change. En cas de réponse signalant l'expiration du curseur, l'agent doit conserver l'erreur, puis soit redémarrer depuis un point de contrôle stable, soit terminer le scan comme incomplet. Il ne doit pas avancer en devinant un nouveau curseur.

Un redémarrage peut aussi donner une fausse impression d'exhaustivité. Si des enregistrements ont changé entre le premier passage et la reprise, la liste combinée peut contenir des trous ou des doublons. Signalez la reprise et la condition utilisée, par exemple `created_at \u003e= last_observed_timestamp`. S'il n'existe aucune méthode de reprise stable, indiquez que la collection a changé pendant le parcours et n'utilisez pas le résultat comme liste de suppression.

## La pagination par offset dérive lorsque les collections changent

La pagination par offset utilise un nombre comme `offset=200\u0026limit=50` ou `page=5\u0026per_page=50`. Elle est facile à automatiser et à expliquer, ce qui explique qu'elle reste courante. Elle devient aussi peu fiable lorsque de nouveaux enregistrements arrivent ou que d'anciens disparaissent pendant le parcours.

Supposons que la première page renvoie les enregistrements 1 à 50, du plus récent au plus ancien. Avant la demande de la deuxième page, dix nouveaux enregistrements arrivent. `offset=50` commence alors après les enregistrements nouvellement insérés et recoupe des objets déjà vus. Si des enregistrements disparaissent de la première page, le même offset peut sauter ceux qui ont remonté. La déduplication des identifiants ne suffit pas : elle détecte les répétitions, pas les omissions.

Si l'API autorise un tri stable, choisissez-en un avec un critère de départage déterministe. `created_at` seul ne suffit souvent pas, car plusieurs enregistrements peuvent partager le même horodatage. Un tri comme `created_at,id`, lorsqu'il est documenté par le fournisseur, permet à l'agent d'enregistrer un point haut et de reprendre plus prudemment. Si l'API ne fournit qu'un offset, sans instantané ni ordre stable, soyez prudent avec les conclusions tirées de plusieurs pages.

Pour une tâche qui exige une réponse complète, utilisez l'une de ces méthodes, par ordre décroissant de confiance :

1. Demandez au service un instantané, une tâche d'export ou un curseur qui documente une vue stable.
2. Limitez la requête à une période immuable et utilisez un ordre stable documenté.
3. Effectuez un second scan, comparez les identifiants et signalez tout désaccord.
4. Demandez à un humain d'approuver un périmètre plus étroit et clairement défini au lieu d'effectuer une modification large.

Ne transformez pas une mauvaise interface en workflow destructif. Un agent peut tout de même utiliser des pages par offset pour échantillonner, localiser un objet précis ou produire un inventaire partiel. Il ne doit pas utiliser un scan par offset instable pour prouver que chaque identifiant, projet ou utilisateur correspondant a été trouvé.

## La fin doit venir du protocole, pas d'une intuition

Les API expriment l'état de pagination à des endroits différents. Le corps JSON peut contenir `next_cursor`, `has_more` ou une URL vers la page suivante. D'autres API utilisent l'en-tête HTTP `Link`. La RFC 8288 définit le Web Linking et le paramètre `rel`, utilisé pour des relations comme `next`. L'en-tête fournit une relation, pas la garantie que le corps de la réponse contiendra un champ de curseur familier.

Un agent a besoin d'une règle de fin propre au endpoint. Écrivez-la à côté de la définition de la requête. Par exemple : « Terminer lorsque `next_cursor` est null. » Ou : « Terminer lorsqu'aucune relation Link ne contient `rel="next"`. » N'écrivez pas : « Terminer lorsqu'il y a moins de 100 enregistrements. » Ce raccourci échoue avec les pages filtrées, la réduction liée aux permissions, les plafonds du service et les API qui renvoient volontairement des pages de tailles variables.

Un en-tête Link courant peut ressembler à ceci :

```http
Link: \u003c/v1/events?limit=100\u0026cursor=a6f3\u003e; rel="next",
      \u003c/v1/events?limit=100\u0026cursor=first\u003e; rel="first"
```

L'agent doit sélectionner uniquement la relation qu'il comprend. Il ne doit pas concaténer l'en-tête, supposer que le lien `first` constitue un point de reprise sûr ou déduire que l'absence d'un lien `last` signifie qu'il n'y a pas de dernière page. C'est la documentation de l'API qui régit le contrat de pagination ; la RFC 8288 décrit seulement la manière dont les relations de liens circulent dans les en-têtes HTTP.

Certaines API renvoient `has_more: true` avec une page vide. Cela semble absurde jusqu'à ce qu'un filtre de permissions, une suppression concurrente ou un index retardé entre en jeu. Si l'API documente ce comportement, continuez avec le jeton de continuation tant que le budget le permet et enregistrez la page vide. Si elle ne le documente pas, arrêtez-vous et signalez une réponse de pagination incohérente. Continuer indéfiniment parce que `has_more` reste à true est une erreur de programmation, pas de la persévérance.

Distinguez aussi une réponse finale d'un statut HTTP réussi. `200 OK` indique que la requête concernée a réussi. Il ne signifie pas que la collection est terminée. Un `404` peut indiquer un curseur expiré, une erreur de endpoint ou une ressource disparue. Conservez le statut, le corps de la réponse et le dernier curseur dans l'enregistrement d'exécution afin qu'un humain puisse comprendre ce qui s'est produit.

## Un résultat partiel a besoin d'une déclaration de limite

Un agent doit rendre compte de la découverte comme le ferait un opérateur rigoureux dans une note d'incident : préciser ce qu'il a demandé, ce qu'il a observé et ce qu'il n'a pas inspecté. La plupart des mauvais rapports échouent sur le dernier point. Ils listent les résultats, mais omettent le curseur, le plafond ou l'erreur qui les rend incomplets.

Utilisez un format qu'un humain peut exploiter sans reconstruire la session :

```text
Scope: GET /v1/projects?state=active\u0026sort=id
Requested page size: 50
Pages fetched: 10
Records received: 487
Completion: partial
Stop reason: page budget reached
Last continuation cursor: eyJvcmRlciI6ImlkIiwicG9zIjoiNTg3In0
Observed finding: 12 projects matched the review rule
Uninspected scope: records after the last continuation cursor
Action taken: none
```

La dernière ligne compte. Une exécution de découverte doit préciser si elle a modifié quelque chose. La personne qui examine le rapport ne devrait jamais avoir à deviner si l'agent s'est contenté de lister des objets ou s'il a agi sur eux.

N'exposez pas un curseur sensible dans une conversation si le fournisseur le traite comme une capacité bearer ou si son contenu peut révéler la structure d'un compte. Stockez le jeton exact dans des métadonnées d'exécution protégées, puis présentez son empreinte ou un préfixe masqué dans le rapport destiné aux humains. L'agent a besoin de suffisamment d'état pour reprendre ou auditer le parcours, mais les personnes n'ont pas besoin de voir des jetons de continuation dispersés dans des tickets et des terminaux.

Le choix des mots réduit ici le risque. « Je n'ai trouvé aucun enregistrement correspondant parmi les 500 premiers inspectés » est exact. « Il n'existe aucun enregistrement correspondant » ne l'est qu'après un parcours complet dans une vue suffisamment stable. La nuance semble tatillonne jusqu'à ce qu'une décision de nettoyage ou de conformité en dépende.

## Les limites de débit et les reprises ont leurs propres règles d'arrêt

La pagination amplifie les erreurs de limite de débit, car une seule requête devient une boucle. Un agent qui reçoit `429 Too Many Requests` ne doit pas marteler le endpoint avec le même curseur. Respectez `Retry-After` lorsque le serveur le fournit, comptez l'attente dans le délai d'exécution et arrêtez-vous lorsque le budget de nouvelles tentatives est épuisé.

Pour les erreurs transitoires comme un délai d'attente ou une réponse `5xx`, réessayez la même page avant d'avancer. Si l'API prend en charge un identifiant d'idempotence ou de requête pour les appels de liste, utilisez-le conformément à sa documentation. N'avancez jamais vers le curseur suivant après une réponse incertaine simplement parce que la requête a peut-être réussi. Vous créeriez un trou silencieux.

Une politique de reprise bornée peut prévoir :

- Réessayer la page actuelle au maximum deux fois après une erreur de transport ou de serveur transitoire.
- Respecter `Retry-After` pour les limites de débit si le délai restant le permet.
- Ne pas réessayer les erreurs d'authentification ou d'autorisation sans changement de l'état d'autorisation.
- S'arrêter en cas de données de pagination mal formées, de curseur répété ou de réponse de continuation non documentée.

Un curseur répété mérite une attention particulière. Si la troisième page renvoie le même `next_cursor` que celui soumis par l'agent, continuer peut créer une boucle infinie. Comparez chaque nouveau curseur à celui qui a été envoyé et à l'ensemble des curseurs précédents. Arrêtez-vous en cas de répétition, sauf si le fournisseur documente un cas où elle est attendue, ce qui est rare et doit être géré par une règle propre au fournisseur.

L'agent doit conserver suffisamment de métadonnées de réponse pour diagnostiquer une reprise sans stocker de secrets. Enregistrez le code d'état, le chemin de la requête, les en-têtes sélectionnés et non sensibles, le numéro de séquence de la page, l'empreinte du curseur, les horodatages et, si la politique l'autorise, une empreinte du corps de réponse. Ne copiez pas les en-têtes d'autorisation, les jetons bearer complets ou les URL contenant des identifiants dans les journaux simplement parce qu'une requête a échoué.

## Un exemple d'échec montre pourquoi la première page est dangereuse

Prenons un agent chargé de désactiver chaque intégration inactive depuis une date donnée. Le endpoint des intégrations renvoie par défaut 25 éléments, les trie du plus récemment modifié au plus ancien et fournit `next_cursor` uniquement lorsqu'il existe d'autres pages. L'agent récupère la première page, trouve trois intégrations inactives et les désactive. Il signale ensuite avoir nettoyé les intégrations inactives.

Ce rapport est faux pour deux raisons. L'agent n'a pas inspecté toutes les intégrations et il a agi pendant la découverte. Le fait d'avoir trouvé trois candidats ne dit rien des pages suivantes. Pire encore, la désactivation modifie `updated_at`, ce qui peut réordonner la collection si le endpoint utilise son tri par défaut. L'agent a rendu son propre parcours moins stable.

Une exécution plus sûre commence par un filtre explicite et un ordre stable, si l'API le permet :

```http
GET /v1/integrations?status=inactive\u0026updated_before=2024-01-01\u0026limit=50\u0026sort=id HTTP/1.1
```

L'agent rassemble les identifiants page après page sans les modifier. Il s'arrête uniquement lorsque le serveur n'émet plus de curseur suivant ou lorsque son budget de découverte est atteint. Il signale alors un ensemble complet ou partiel de candidats. Une requête d'action distincte peut utiliser les identifiants réunis, idéalement après qu'un humain a vu leur nombre et leur périmètre.

Si le endpoint de liste ne prend pas en charge un ordre stable, l'agent doit le dire. Il peut toujours rassembler des candidats, mais ne doit pas prétendre disposer d'un ensemble complet et exempt de conditions de concurrence. Le conseil populaire « agir dès que les éléments sont trouvés » semble efficace parce qu'il évite un second passage. Il est inadapté aux listes modifiables lorsque l'action change la position, l'éligibilité ou les permissions.

Le même principe s'applique aux alertes de sécurité, aux comptes utilisateurs, aux enregistrements de déploiement et aux artefacts de build. Lire d'abord, définir le périmètre, puis modifier. Il existe des exceptions pour un confinement urgent, comme la révocation d'un identifiant compromis précisément nommé, mais ce n'est pas une tâche de nettoyage paginée. C'est une action ciblée sur un identifiant connu.

## Gardez les identifiants et l'observabilité hors de l'agent

Un agent ne devrait pas avoir besoin du secret API pour paginer. Le composant qui exécute les requêtes HTTP peut injecter les identifiants, appliquer l'autorisation humaine lorsque nécessaire et enregistrer la séquence réelle des requêtes. L'agent reste ainsi concentré sur la construction et l'interprétation des requêtes, sans manipuler des jetons qui lui permettraient d'effectuer ailleurs des appels sans limite.

Pour les équipes qui utilisent Sallyport, les appels HTTP peuvent passer par sa passerelle d'actions, tandis que les identifiants restent dans le coffre chiffré et que le journal Activity enregistre chaque appel. Cette trace aide un contrôleur à comparer le nombre de pages annoncé par l'agent avec les requêtes réellement exécutées, mais elle ne remplace pas les budgets de pages dans les instructions de l'agent.

Gardez la politique de parcours près de la définition de la tâche. Précisez les endpoints autorisés, les filtres, les champs, le nombre maximal de pages, le comportement des reprises et la déclaration de limite requise. Une couche d'autorisation générale ne peut pas décider si le scan d'un dépôt est terminé après une page ou si un inventaire de conformité exige toutes les pages.

Le journal de session et la piste d'activité par appel de Sallyport peuvent rendre la révocation et l'examen pratiques lorsqu'une exécution d'agent dévie. L'agent doit toutefois recevoir l'instruction de s'arrêter face à une forme de curseur inconnue, à un budget épuisé ou à une réponse API contraire au contrat documenté du endpoint. Enregistrer après coup un mauvais parcours vaut mieux que ne conserver aucune preuve, mais cela n'annule ni les appels inutiles ni une action incorrecte.

## Testez le parcours avec des scénarios de pagination hostiles

La pagination du scénario normal cache les défauts qui comptent. Avant de faire confiance à un workflow d'agent, testez-le avec des scénarios qui renvoient une première page vide avec un curseur, une page courte avec d'autres résultats, un élément dupliqué, un curseur répété, un curseur expiré et une réponse de limite de débit entre deux pages normales.

Le comportement attendu doit être précis et prévisible. L'agent ne continue après une page vide que lorsque l'état de continuation documenté l'y autorise. Il déduplique les identifiants ou s'arrête en cas de répétition selon l'exigence de cohérence de la tâche. Il n'invente jamais de curseur après une expiration et signale une couverture partielle lorsqu'une limite de sécurité termine l'exécution.

Utilisez ce tableau de validation pour examiner une boucle d'appel d'outil :

| Scénario | Résultat attendu |
| --- | --- |
| 12 enregistrements, aucun curseur suivant | Terminer après une requête |
| 12 enregistrements, curseur suivant présent | Continuer malgré la page courte |
| Même curseur renvoyé deux fois | S'arrêter et signaler une boucle de pagination |
| `429` avec `Retry-After` | Attendre uniquement dans le délai, puis réessayer la page actuelle |
| Curseur rejeté comme expiré | Reprendre uniquement via un point de contrôle documenté, ou signaler un résultat incomplet |

Examinez les appels bruts autant que le texte final. Un rapport soigné peut dissimuler un curseur sauté ou une requête supplémentaire après la condition d'arrêt. Le journal d'exécution doit montrer une requête initiale, chaque requête de continuation, toute reprise du même curseur et aucun appel après la réponse finale.

Définissez un petit budget par défaut pour le travail exploratoire et exigez une modification explicite de la tâche pour une énumération large. Cette seule contrainte évite les deux échecs qui font perdre le plus de temps : les agents qui scannent indéfiniment et ceux qui confondent discrètement la première page avec la réponse complète.
