# Tâches API asynchrones : des workflows d'agents traçables

Un agent qui soumet une requête API longue a besoin d'un workflow, pas d'une boucle qui appelle les endpoints jusqu'à ce que quelque chose semble terminé. La création, l'observation, l'annulation et la récupération du résultat ont chacune leurs propres modes d'échec. Si vous les réduisez à un prompt et quelques nouvelles tentatives, vous finirez par lancer deux fois le même traitement, perdre un résultat terminé ou annoncer une annulation qui n'a jamais été effective.

La difficulté ne réside pas dans l'envoi d'une requête HTTP. Il faut préserver l'intention après le redémarrage d'un agent, un délai d'attente réseau, une panne du fournisseur ou l'intervention d'une personne qui dit « arrêtez » alors que le service distant a déjà commencé à travailler. Construisez le workflow autour d'un enregistrement local durable de la tâche et faites en sorte que chaque appel externe puisse être expliqué plus tard : qu'avons-nous demandé, quelle tâche distante en est responsable, quel état avons-nous observé et qu'avons-nous fait ensuite ?

## Une requête de création doit établir la propriété

Un endpoint de création lance une opération asynchrone et renvoie une réponse avant que l'opération n'atteigne un état final. Sa première réponse doit donner à l'agent assez d'informations pour continuer sans répéter la requête. Dans une API bien conçue, cela signifie un identifiant de tâche, un état initial et soit une URL de statut, soit une convention d'endpoint permettant de récupérer la tâche.

Ne déduisez pas qu'une requête a été acceptée parce qu'une connexion a été établie ou parce que le client a dépassé le délai après l'envoi des octets. La seule preuve utile est une réponse du serveur ou une recherche ultérieure qui relie la requête logique d'origine à une tâche. La livraison réseau est incertaine précisément au moment où le client voudrait obtenir une réponse simple.

Attribuez un identifiant d'opération local à chaque opération logique avant que l'agent n'envoie la requête. Cet identifiant vous appartient, pas au fournisseur. Enregistrez-le avec le corps de la requête ou une empreinte normalisée de ce corps, la cible prévue, le jeton d'idempotence, les horodatages et l'appelant qui a autorisé l'opération. Écrivez durablement cet enregistrement avant d'envoyer la requête.

Un enregistrement minimal peut ressembler à ceci :

```json
{
  "operation_id": "op_01J7Q5X4D4PA3D",
  "request_fingerprint": "sha256:4f8b...",
  "idempotency_token": "idem_5b5c76c7",
  "remote_job_id": null,
  "state": "create_pending",
  "created_at": "2025-03-08T14:22:11Z",
  "create_deadline": "2025-03-08T14:24:11Z",
  "result_deadline": "2025-03-08T15:22:11Z"
}
```

L'empreinte permet de détecter une erreur subtile mais fréquente : un agent réessaie un appel de création après avoir modifié un paramètre. Il s'agit d'une nouvelle opération, même si une personne parlerait de « la même tâche ». Un jeton associé à une forme de requête ne doit jamais autoriser discrètement une autre requête.

Une réponse de création peut être :

```http
HTTP/1.1 202 Accepted
Location: /v1/jobs/job_7ad2
Content-Type: application/json

{
  "job_id": "job_7ad2",
  "state": "queued",
  "status_url": "/v1/jobs/job_7ad2"
}
```

Dès que la réponse arrive, mettez à jour l'enregistrement durable avec `remote_job_id`, l'état observé et les métadonnées de la réponse. L'agent peut alors passer à l'observation. Si l'API renvoie plutôt un succès synchrone, enregistrez le résultat sous la même opération. Le workflow doit prendre en charge les deux chemins sans prétendre qu'ils signifient la même chose.

## L'idempotence sert aux livraisons incertaines, pas aux nouvelles tentatives en général

Un jeton d'idempotence indique au serveur que la livraison répétée d'une même requête logique de création ne doit pas produire plusieurs fois le traitement. Il ne rend pas toutes les requêtes sûres et ne répare pas une API qui n'a jamais implémenté l'idempotence sur son endpoint de création.

Le jeton doit figurer dans la requête de création conformément au contrat du fournisseur. Certaines API acceptent un en-tête `Idempotency-Key`. D'autres exigent un champ dans la requête. Utilisez la forme documentée et générez un jeton avec suffisamment d'entropie pour éviter toute collision entre opérations distinctes. Conservez le même jeton jusqu'à la résolution de l'opération d'origine.

```http
POST /v1/reports HTTP/1.1
Content-Type: application/json
Idempotency-Key: idem_5b5c76c7
X-Trace-ID: tr_0830d3

{
  "account": "acct_218",
  "range": {"start": "2025-02-01", "end": "2025-02-28"},
  "format": "csv"
}
```

La séquence de nouvelle tentative sûre est limitée :

1. Générez le jeton et enregistrez l'opération.
2. Envoyez la requête de création avec ce jeton.
3. Si la réponse est perdue ou si le client dépasse le délai, renvoyez la requête identique avec le même jeton.
4. Si le serveur renvoie la tâche d'origine, enregistrez son identifiant et continuez.
5. Si vous avez besoin d'autres données, clôturez ou annulez si possible l'ancienne opération, puis créez un nouvel enregistrement et un nouveau jeton.

Cette distinction compte, car les agents réécrivent naturellement les requêtes pendant leur raisonnement. Modifier une plage de dates, une destination, un compte ou un format de sortie modifie l'effet de la requête. Réutiliser un jeton après un tel changement crée un désaccord entre le client et le serveur : un serveur rigoureux rejette la différence, tandis qu'un serveur moins rigoureux peut renvoyer une ancienne réponse qui ne correspond plus à l'intention de l'agent.

La norme HTTP établit clairement cette distinction. La RFC 9110 définit les méthodes idempotentes comme celles dont l'effet attendu après plusieurs requêtes identiques reste le même qu'après une seule. POST n'est pas idempotente par défaut. Un fournisseur peut ajouter un comportement d'idempotence à un endpoint POST, mais le client doit le considérer comme un contrat applicatif explicite, et non comme une règle HTTP.

Un conseil souvent répété à tort dit : « Ne réessayez POST qu'une seule fois. » Le nombre de tentatives n'est pas le point essentiel. Un seul envoi en double peut déclencher un paiement, configurer un environnement ou lancer un traitement coûteux. Réessayez une requête de création lorsque l'échéance et les recommandations du fournisseur le permettent, mais uniquement avec un jeton qui permet au serveur de reconnaître l'opération d'origine.

## Un délai d'attente laisse l'état de la tâche inconnu

Le dépassement du délai de création ne signifie pas que le service a rejeté la requête. Cela signifie que l'agent n'a pas reçu de réponse définitive avant sa propre échéance. Le serveur a peut-être accepté la requête, est peut-être encore en train de la traiter ou ne l'a peut-être jamais reçue.

C'est l'échec qui révèle les faiblesses d'un workflow d'agent. L'agent envoie une requête de création, attend trente secondes, ne reçoit aucune réponse et envoie une nouvelle requête avec un nouveau jeton. Deux rapports s'exécutent alors. Le second peut finir en premier, ce qui rend l'incident difficile à repérer avant la comparaison des factures, des exports ou des modifications en aval.

Conservez un état explicite `create_pending`. Lorsque l'appel échoue de manière ambiguë, enregistrez la catégorie d'erreur, l'heure et le nombre de tentatives, mais ne supprimez pas l'opération. Utilisez ensuite le mécanisme de rapprochement de l'API. Les fournisseurs proposent différentes solutions :

- Une nouvelle tentative avec le même jeton d'idempotence peut renvoyer la réponse d'acceptation d'origine.
- Un endpoint de liste ou de recherche peut filtrer par référence de requête client.
- Une recherche de statut peut accepter un identifiant d'opération fourni par le client.
- Le fournisseur peut documenter une requête exploitable pour rechercher les créations récentes à partir d'un identifiant de requête.

Si aucun de ces mécanismes n'existe, l'API ne peut pas offrir au client une garantie fiable de création au plus une fois lorsqu'une réponse est perdue. Dites-le clairement dans votre conception. Vous pouvez réduire les doublons avec une outbox locale et des tentatives mesurées, mais vous ne pouvez pas prouver qu'une nouvelle tentative n'a pas créé un traitement supplémentaire.

Traitez comme un incident nécessitant une intervention le retour, avec le même jeton, d'un identifiant de tâche inconnu. N'écrasez pas l'ancien identifiant. Conservez les deux réponses, arrêtez l'activité automatique pour cette opération et demandez une décision humaine. Choisir discrètement l'un des deux est la manière dont les pistes d'audit deviennent fictives.

Utilisez des échéances qui distinguent la communication du traitement. L'échéance de création indique pendant combien de temps l'agent essaiera d'obtenir un identifiant de tâche distante. L'échéance de résultat indique pendant combien de temps le processus métier attendra la fin du traitement. Une tâche peut survivre à un bref délai de réponse lors de la création et disposer encore de plusieurs heures pour s'achever. Réunir les deux dans un seul minuteur pousse les agents à abandonner des traitements récupérables ou à les relancer au mauvais moment.

## Les interrogations ont besoin d'un délai progressif, d'un propriétaire et d'une heure d'arrêt

Les interrogations sont sûres lorsqu'un workflow durable est responsable de la tâche et que chaque interrogation consigne une observation. Elles deviennent abusives lorsque plusieurs exécutions d'agents redécouvrent la même tâche et l'interrogent chacune de leur côté.

Placez l'identifiant de tâche distante dans un seul enregistrement et attribuez un bail au processus qui supervise actuellement l'observation. Ce bail peut être une ligne de base de données avec un horodatage d'expiration, un message de file avec des règles de visibilité ou un autre mécanisme durable de contrôle de la concurrence. Si le worker tombe en panne, un autre peut reprendre après l'expiration du bail. Sans propriétaire, les nouvelles tentatives et les redémarrages multiplient les appels de statut.

Respectez `Retry-After` lorsque l'API l'envoie. Si l'API ne fournit aucune indication, utilisez un délai exponentiel plafonné avec une part d'aléatoire. Le plafond exact dépend de la rapidité avec laquelle le métier a besoin d'une réponse et des limites de débit du fournisseur, mais la progression doit éviter les pics synchronisés.

```text
attempt 1: wait a randomized interval near 2 seconds
attempt 2: wait a randomized interval near 4 seconds
attempt 3: wait a randomized interval near 8 seconds
later attempts: keep increasing until the configured cap
```

Ne calculez pas le prochain délai à partir du résumé rédigé par l'agent. Enregistrez la prochaine heure d'interrogation dans l'enregistrement de la tâche. Un worker redémarré peut ainsi reprendre la planification, et un opérateur peut comprendre clairement pourquoi l'agent attend.

Une réponse de statut doit mettre à jour uniquement les faits observés. Par exemple :

```json
{
  "job_id": "job_7ad2",
  "state": "running",
  "updated_at": "2025-03-08T14:26:40Z",
  "progress": {"completed": 146, "total": 500}
}
```

Enregistrez `state`, l'horodatage du fournisseur s'il est fourni, l'heure de récupération, la référence de la réponse brute et l'action suivante. Ne transformez pas un champ de progression vague en promesse de fin prochaine. Les fournisseurs signalent souvent l'avancement en retard ou par lots. La progression aide les opérateurs, mais c'est l'état final qui contrôle le workflow.

Définissez une échéance de résultat et faites de son expiration un état, pas une excuse pour oublier la tâche. `result_timed_out` signifie que l'agent a cessé les interrogations automatiques parce que son accord a expiré. Cela ne signifie pas que la tâche distante s'est arrêtée. Si l'action a un coût réel ou des effets de bord, conservez assez d'informations pour effectuer un rapprochement ultérieur et décider si une annulation est appropriée.

Les webhooks peuvent réduire la latence, mais ils ne suppriment pas la boucle de statut. Les fournisseurs peuvent réessayer les callbacks, les livrer dans le désordre ou ne pas les livrer. Vérifiez le callback conformément à la documentation du fournisseur, dédupliquez-le avec un identifiant d'événement lorsqu'il existe, mettez à jour le même enregistrement de tâche et effectuez une lecture finale du statut avant de déclarer le succès.

## Les transitions d'état doivent rejeter les suppositions

Une machine à états protège le workflow contre un agent qui interpréterait les mots avec trop de liberté. Définissez vos états locaux et les transitions autorisées avant de connecter les outils à l'API. Les services distants utilisent des noms différents, mais votre enregistrement doit rendre l'incertitude visible.

Un modèle local pratique est le suivant :

```text
create_pending -> accepted -> observing -> result_collecting -> succeeded
create_pending -> create_unknown -> reconciliation
accepted or observing -> cancel_requested -> cancelling -> cancelled
accepted or observing -> failed
observing -> result_timed_out
```

Les flèches sont des règles, pas un schéma destiné à la documentation. Un worker doit rejeter toute transition qui ne repose sur aucune preuve. Il ne peut pas marquer `succeeded` parce qu'il a vu une progression de 100. Il ne peut pas marquer `cancelled` parce qu'il a envoyé `DELETE /jobs/job_7ad2`. Il ne peut pas passer de `failed` à `observing` sauf si l'API distante prend explicitement en charge une nouvelle tentative ou une reprise et que la nouvelle action est enregistrée séparément.

Conservez séparément l'état distant et l'état local. `cancel_requested` décrit un fait local : l'agent a envoyé une demande d'annulation et attend sa confirmation. `cancelled` décrit un fait distant : le service a signalé un état final annulé. Cette petite distinction évite beaucoup de confusion pendant un incident.

Utilisez un historique des transitions en ajout uniquement. Chaque entrée doit contenir l'identifiant de l'opération, l'acteur, l'heure, l'état local précédent, le nouvel état local, la requête ou la réponse déclenchante et la raison. Un historique compact suffit :

```json
{
  "at": "2025-03-08T14:29:02Z",
  "actor": "worker-3",
  "from": "observing",
  "to": "cancel_requested",
  "cause": "human_request:req_91af",
  "remote_job_id": "job_7ad2"
}
```

N'utilisez pas un unique champ `status` modifiable comme seul enregistrement. Il indique ce que le workflow croit maintenant, mais pas pourquoi il le croyait cinq minutes plus tôt. Lorsqu'une API distante renvoie ensuite un état surprenant, l'historique permet de savoir si le fournisseur a changé, si l'agent a répété un appel ou si un opérateur est intervenu.

## L'annulation nécessite une confirmation et une limite de dommages

Une annulation est une demande d'arrêter le travail à venir. Elle ne peut pas annuler ce que le fournisseur a déjà engagé, et certains fournisseurs permettent une course dans laquelle la tâche se termine au moment où la demande d'annulation arrive. Construisez le workflow en tenant compte de cette réalité.

Lorsqu'une personne ou une règle décide d'arrêter une tâche, enregistrez d'abord l'intention d'annulation. Indiquez qui a fait la demande, pourquoi et quel effet est attendu. Appelez ensuite l'endpoint d'annulation documenté avec l'identifiant de tâche distante enregistré. Conservez la réponse, même si elle indique seulement que le serveur a accepté la demande.

Continuez les interrogations après l'annulation. Les résultats finaux acceptables comprennent généralement `cancelled`, `succeeded` et `failed`. Un résultat terminé après une demande d'annulation n'est pas automatiquement une erreur. Il peut être le résultat réel d'une tâche qui a franchi son point d'engagement quelques secondes plus tôt. Le workflow doit rapporter fidèlement cette séquence, sans réécrire l'historique pour correspondre au résultat souhaité.

Certaines opérations ont besoin d'une limite de dommages distincte de l'annulation. Si une tâche d'export écrit un fichier, son annulation peut laisser un fichier partiel. Si une tâche de provisionnement crée des ressources, l'annulation peut laisser certaines ressources en place. Le contrat de l'API doit indiquer si elle propose un nettoyage, une restauration ou des détails sur les résultats partiels. Dans le cas contraire, considérez l'annulation comme un contrôle opérationnel, et non comme une transaction.

N'envoyez pas plusieurs demandes d'annulation depuis chaque worker d'interrogation. Enregistrez `cancel_requested`, rendez l'opération d'annulation idempotente si le fournisseur le permet et laissez le propriétaire du bail assurer le suivi. Répéter une requête inoffensive gaspille des ressources ; répéter une annulation ayant des effets de bord peut brouiller le journal d'audit distant.

Une échéance d'annulation est également utile. Après un délai raisonnable et documenté, passez à `cancellation_unconfirmed` au lieu de revendiquer le succès. Escaladez avec l'identifiant de tâche distante, l'identifiant de trace, l'historique des requêtes et les identifiants de requête du fournisseur. Cet ensemble permet à une personne ou à l'équipe d'assistance du fournisseur de voir la séquence réelle sans devoir la reconstituer à partir de messages de discussion.

## La récupération du résultat est une action distincte

Un état final de succès signifie que le traitement distant est terminé. Il ne garantit pas que le résultat a été récupéré, validé, stocké ou transmis au système suivant. Traitez la récupération comme une action enregistrée séparément.

Commencez par récupérer le résultat avec l'identifiant de tâche ou la référence de résultat fournie par l'API. Validez le type de contenu, le schéma, la somme de contrôle, la taille ou le nombre d'enregistrements attendu lorsque le fournisseur en fournit un. Enregistrez la référence du résultat et le résultat de la validation dans l'enregistrement de la tâche. Si le résultat est volumineux, stockez un emplacement durable et les données d'intégrité plutôt que de copier un contenu opaque dans un journal d'événements.

Déterminez ensuite si la récupération elle-même nécessite de l'idempotence. De nombreux endpoints de résultat sont des lectures sûres. D'autres génèrent un téléchargement temporaire, consomment un artefact à usage unique ou marquent une tâche comme livrée. Consultez le contrat. Un agent qui considère chaque `GET` comme inoffensif peut tout de même déclencher une modification d'état propre au fournisseur.

N'utilisez pas un statut HTTP réussi comme seule validation. Un endpoint de rapport peut renvoyer un fichier valide contenant une ligne d'erreur. Un lot d'images peut renvoyer un manifeste avec des éléments échoués. Un export de données peut être terminé tout en omettant des enregistrements auxquels l'API indique que l'appelant n'a pas accès. Validez le résultat par rapport à l'attente métier à l'origine de la tâche.

Pour les traitements par lots, enregistrez les résultats élément par élément lorsque le fournisseur le permet. Une tâche finale peut contenir 498 réussites et deux échecs. La qualifier simplement de « réussie » oblige l'agent suivant à redécouvrir l'échec partiel dans le contenu du résultat. Votre état local final peut rester réussi, tandis que le résumé du résultat contient les compteurs et la liste des références des éléments échoués.

Clôturez l'opération uniquement lorsque la récupération respecte son contrat. `succeeded` doit signifier que le résultat attendu est disponible et vérifié selon vos règles. Si le fournisseur a terminé mais que la récupération a échoué, utilisez un état local distinct comme `result_unavailable` ou `result_validation_failed`. La tâche distante peut être terminée alors que votre workflow ne l'est pas.

## Les identifiants de trace relient les actions, les audits établissent les faits

Utilisez un identifiant de trace pour chaque opération et envoyez-le lors de la création, des lectures de statut, de l'annulation et de la récupération lorsque l'API accepte les en-têtes personnalisés. Associez-le à l'identifiant de tâche distante dès que vous le connaissez. L'identifiant de trace relie les événements dans vos systèmes ; l'identifiant de tâche permet au fournisseur de retrouver son propre élément de travail.

Ne confondez pas ces deux identifiants. Un identifiant de trace ne doit pas devenir un jeton d'idempotence, car une opération peut comporter plusieurs requêtes avec des règles de nouvelle tentative différentes. Un identifiant de tâche ne doit pas devenir votre preuve d'autorisation, car le fournisseur l'a généré après votre décision locale d'agir.

Votre enregistrement d'audit doit répondre à des questions auxquelles les journaux ne répondent souvent pas : quel processus d'agent a lancé l'opération, quelle approbation humaine la couvrait, quelle action authentifiée a été effectuée et quelqu'un a-t-il modifié l'historique après coup ? Écrivez un enregistrement concis de l'intention avant l'appel de création, puis ajoutez les observations. Conservez les identifiants de requête et les métadonnées de réponse assainies. Ne placez jamais de jetons bearer, mots de passe, clés privées ou charges utiles sensibles complètes dans un journal général.

Sallyport peut exécuter des appels API HTTP sans exposer les identifiants stockés à l'agent, et ses sessions ainsi que ses appels individuels fournissent une piste infalsifiable qui peut être vérifiée avec `sp audit verify`. Cela protège la conservation des identifiants et les preuves d'action ; votre workflow a néanmoins besoin de son propre enregistrement d'opération, car lui seul sait si `job_7ad2` correspond à la tâche métier demandée.

Une trace n'est utile lors d'une journée difficile que si chaque composant l'enregistre de manière cohérente. Placez-la dans l'enregistrement local de la tâche, le contexte d'exécution de l'agent, les en-têtes des requêtes lorsque cela est permis, les annotations d'audit lorsque cela est permis et les tickets des opérateurs. Ne créez pas une nouvelle trace pour chaque interrogation. Ce sont des événements enfants de la même opération.

## Une boucle de référence gère les échecs courants

Le workflow ci-dessous rend explicites les décisions difficiles. Il suppose un fournisseur avec un contrat de création idempotent, un endpoint de statut et un endpoint d'annulation. Adaptez les noms des endpoints, mais ne supprimez pas les transitions d'état persistantes.

```text
load operation by local operation ID

if no operation exists:
    create and persist record with fingerprint and idempotency token

if remote job ID is absent:
    send create with the stored token
    if response confirms job ID:
        persist job ID and move to observing
    if response is ambiguous:
        move to create_unknown and reconcile using the stored token
    if response rejects request definitively:
        move to failed

while local state requires observation and result deadline has not passed:
    acquire lease for the operation
    read remote status
    append the observation
    if cancellation was requested and remote state is nonterminal:
        send cancellation once and record the attempt
    if remote state is terminal:
        collect and validate result if appropriate
        persist final local state
    otherwise:
        persist next poll time and release lease

if the deadline expires before a terminal observation:
    move to result_timed_out and preserve the reconciliation record
```

Cette boucle ne comporte aucun nombre magique de tentatives, car les limites dépendent du fournisseur, du coût de l'opération et de l'échéance de l'appelant. Elle applique toutefois une règle plus importante : chaque nouvelle tentative se rapporte à une opération enregistrée, et chaque action visible à l'extérieur modifie l'historique de cette opération.

Testez le workflow avec injection d'erreurs avant de le confier à des agents autonomes. Supprimez la réponse de création après l'acceptation par le serveur. Arrêtez le worker après l'enregistrement de l'identifiant de tâche, mais avant la planification de la première interrogation. Renvoyez un résultat final pendant une course d'annulation. Livrez deux fois le même webhook. Redémarrez avec un bail périmé. Si le workflow ne peut pas expliquer et gérer chacun de ces cas, il n'est pas prêt à lancer des tâches coûteuses ou lourdes de conséquences.

La première tâche d'implémentation est peu spectaculaire : créez l'enregistrement durable de l'opération et refusez d'envoyer une requête de création sans lui. Cette seule contrainte oblige l'agent à conserver l'intention, rend possible la prévention des doublons et fournit à tous un compte rendu factuel lorsque le système distant se comporte de manière imparfaite.
