Un agent IA ne devient fiable qu'avec un harness d'exécution
Un harness fiable transforme chaque intention en effets idempotents, bornés, traçables et réconciliables, même lorsque le modèle, le réseau ou un outil échoue.
Un modèle peut parfaitement comprendre qu’il faut rembourser une commande, puis déclencher deux remboursements. Il peut produire un plan cohérent, appeler le bon outil et perdre la réponse après que l’effet externe a eu lieu. Il peut également inventer un outil, soumettre un JSON presque valide, interpréter un timeout comme un échec ou poursuivre une boucle dont chaque itération augmente le coût sans améliorer l’état du système.
Aucune de ces défaillances ne se résout uniquement avec un modèle plus performant. Dès qu’un agent agit sur un système externe — paiement, messagerie, CRM, infrastructure, base de données, calendrier — sa fiabilité dépend aussi du logiciel qui encadre son exécution.
Ce logiciel est le harness d’exécution : la couche qui transforme une proposition probabiliste en opération contrôlée. Il sélectionne les outils autorisés, valide leurs arguments, applique des politiques de reprise, impose des budgets, conserve l’état, collecte les preuves et décide si une intention est terminée, rejetée ou encore ambiguë.
Les discussions sur le harness engineering invitent à déplacer l’attention du seul raisonnement du modèle vers le système qui rend son action exploitable. Une autre analyse consacrée aux agents en production souligne le même changement d’échelle : obtenir une réponse est accessible ; coordonner modèles, outils, mémoire, routage, évaluations et validation l’est beaucoup moins.
Cette intuition doit être prolongée jusqu’aux effets externes. Un bon harness ne cherche pas seulement à obtenir une sortie correcte. Il doit garantir quatre propriétés opérationnelles :
- idempotence : répéter une même intention ne crée pas un nouvel effet ;
- bornage : l’agent ne dispose ni d’un temps, ni d’un nombre d’appels, ni d’un coût illimités ;
- traçabilité : chaque décision et chaque tentative peuvent être reliées à leur cause ;
- réconciliation : après une réponse perdue ou contradictoire, le système vérifie l’état réel avant d’agir de nouveau.
Ces propriétés ne rendent pas le modèle déterministe. Elles limitent les conséquences de son indétermination.
Le modèle propose, le harness dispose
Un LLM ne devrait pas posséder directement les capacités d’un opérateur système. Il devrait émettre une proposition d’action structurée : outil souhaité, arguments, intention poursuivie et éléments justifiant l’appel. Le harness reste l’autorité qui accepte, refuse, transforme ou diffère cette proposition.

Cette séparation est essentielle. Le modèle optimise une réponse plausible à partir de son contexte. Le harness applique des règles qui ne doivent pas varier selon la formulation du prompt :
- l’outil existe-t-il dans le registre de cette exécution ?
- l’identité appelante possède-t-elle la permission requise ?
- les arguments respectent-ils le schéma et les invariants métier ?
- l’intention a-t-elle déjà produit un effet ?
- le budget autorise-t-il une nouvelle tentative ?
- une preuve vérifiable confirme-t-elle le résultat ?
Un tool call halluciné devient alors une requête rejetée, pas un appel arbitraire. Un JSON mal formé devient une erreur de protocole, pas une occasion de deviner silencieusement les champs. Un montant négatif, une devise non supportée ou un destinataire hors périmètre sont bloqués avant l’adaptateur réseau.
Le contrat d’un outil doit donc être plus riche qu’un nom accompagné d’une description en langage naturel. Il lui faut au minimum :
- un schéma d’entrée strict, versionné et fermé aux propriétés inconnues ;
- un schéma de sortie distinguant résultat métier et état du transport ;
- des invariants métier indépendants du schéma syntaxique ;
- une classification explicite des erreurs ;
- une politique d’idempotence et de reprise ;
- une portée d’autorisation ;
- un mécanisme de vérification ou de réconciliation ;
- une stratégie de masquage des données sensibles dans les traces.
Le modèle peut corriger un argument rejeté, mais il ne doit pas pouvoir négocier la règle qui l’a rejeté. Autoriser une réparation bornée est utile ; transformer chaque validation en conversation ouverte avec le modèle recrée une boucle sans limite.
Une intention n’est pas une tentative
La distinction la plus importante se trouve entre intention métier et tentative technique.

L’intention est stable : « débiter 49 euros pour la commande order_1842 ». Une tentative est un événement transitoire : premier POST, reprise après timeout, nouvelle exécution d’un worker ou replay d’un workflow durable. Plusieurs tentatives peuvent appartenir à une seule intention, mais elles doivent partager la même clé d’idempotence.
AWS recommande, pour les API idempotentes, un identifiant fourni par l’appelant. Cette approche exprime mieux l’intention qu’un simple hash des paramètres : deux requêtes identiques peuvent représenter deux intentions légitimes, tandis qu’une reprise doit conserver son identifiant initial. AWS précise aussi qu’une même clé accompagnée de paramètres différents doit être traitée comme une incohérence, pas comme une nouvelle demande.
Fait documenté : le contrat d’idempotence doit permettre de reconnaître les doublons, et l’enregistrement du token doit être cohérent avec l’effet qu’il protège. Lorsque le service contrôle sa propre transaction, token et mutation doivent être engagés atomiquement afin d’éviter « token enregistré sans effet » ou « effet produit sans token ».
Choix d’architecture : un harness peut représenter l’identité d’une intention par le triplet tenantId, intentId, operation, puis conserver séparément une empreinte canonique des arguments. La clé identifie l’intention ; l’empreinte détecte sa mutation accidentelle.
Une clé ne doit jamais être régénérée à chaque retry. La documentation des exécutions durables AWS insiste sur la réutilisation d’une clé stable pour les effets critiques : si un replay fabrique une nouvelle clé, la déduplication ne protège plus rien.
Voici une commande TypeScript volontairement centrée sur le contrat du harness. Elle distingue validation, rejet et résultat ambigu sans prétendre qu’un timeout prouve l’absence d’effet.
import { createHash } from "node:crypto";
import { z } from "zod";
const ChargeInput = z.object({
tenantId: z.string().uuid(),
intentId: z.string().min(12).max(120),
orderId: z.string().min(1),
amountMinor: z.number().int().positive(),
currency: z.enum(["EUR", "USD", "XOF"]),
paymentMethodToken: z.string().min(20),
}).strict();
type ChargeInput = z.infer<typeof ChargeInput>;
type ChargeIssue =
| {
issue: "validated";
intentId: string;
idempotencyKey: string;
chargeId: string;
source: "provider" | "deduplicated" | "reconciled";
}
| {
issue: "rejected";
intentId?: string;
code:
| "invalid_arguments"
| "intent_payload_mismatch"
| "policy_denied"
| "budget_exhausted";
retryable: false;
details: string[];
}
| {
issue: "outcome_unknown";
intentId: string;
idempotencyKey: string;
retryable: false;
reconcileAfterMs: number;
reason: "timeout" | "connection_lost" | "provider_unavailable";
};
interface IntentStore {
reserve(input: {
scope: string;
key: string;
payloadHash: string;
}): Promise<
| { state: "new" }
| { state: "completed"; chargeId: string }
| { state: "pending" }
| { state: "mismatch" }
>;
complete(input: {
scope: string;
key: string;
chargeId: string;
}): Promise<void>;
}
interface PaymentProvider {
charge(input: {
amountMinor: number;
currency: ChargeInput["currency"];
paymentMethodToken: string;
idempotencyKey: string;
metadata: { orderId: string; intentId: string };
timeoutMs: number;
}): Promise<{ chargeId: string }>;
}
const stableHash = (value: unknown) =>
createHash("sha256")
.update(JSON.stringify(value))
.digest("hex");
const stableIdempotencyKey = (input: ChargeInput) =>
`charge:${input.tenantId}:${input.intentId}`;
export async function executeCharge(
raw: unknown,
deps: {
intents: IntentStore;
payments: PaymentProvider;
canCharge: (tenantId: string, amountMinor: number) => Promise<boolean>;
remainingToolCalls: number;
},
): Promise<ChargeIssue> {
const parsed = ChargeInput.safeParse(raw);
if (!parsed.success) {
return {
issue: "rejected",
code: "invalid_arguments",
retryable: false,
details: parsed.error.issues.map(
({ path, message }) => `${path.join(".")}: ${message}`,
),
};
}
const input = parsed.data;
if (deps.remainingToolCalls < 1) {
return {
issue: "rejected",
intentId: input.intentId,
code: "budget_exhausted",
retryable: false,
details: ["Aucun appel d'outil restant pour cette exécution."],
};
}
if (!(await deps.canCharge(input.tenantId, input.amountMinor))) {
return {
issue: "rejected",
intentId: input.intentId,
code: "policy_denied",
retryable: false,
details: ["Montant ou compte hors politique d'autorisation."],
};
}
const key = stableIdempotencyKey(input);
const payloadHash = stableHash({
orderId: input.orderId,
amountMinor: input.amountMinor,
currency: input.currency,
paymentMethodToken: input.paymentMethodToken,
});
const reservation = await deps.intents.reserve({
scope: input.tenantId,
key,
payloadHash,
});
if (reservation.state === "mismatch") {
return {
issue: "rejected",
intentId: input.intentId,
code: "intent_payload_mismatch",
retryable: false,
details: ["La même intention a déjà été associée à d'autres arguments."],
};
}
if (reservation.state === "completed") {
return {
issue: "validated",
intentId: input.intentId,
idempotencyKey: key,
chargeId: reservation.chargeId,
source: "deduplicated",
};
}
if (reservation.state === "pending") {
return {
issue: "outcome_unknown",
intentId: input.intentId,
idempotencyKey: key,
retryable: false,
reconcileAfterMs: 2_000,
reason: "provider_unavailable",
};
}
try {
const result = await deps.payments.charge({
amountMinor: input.amountMinor,
currency: input.currency,
paymentMethodToken: input.paymentMethodToken,
idempotencyKey: key,
metadata: {
orderId: input.orderId,
intentId: input.intentId,
},
timeoutMs: 5_000,
});
await deps.intents.complete({
scope: input.tenantId,
key,
chargeId: result.chargeId,
});
return {
issue: "validated",
intentId: input.intentId,
idempotencyKey: key,
chargeId: result.chargeId,
source: "provider",
};
} catch (error) {
const reason =
error instanceof Error && error.name === "TimeoutError"
? "timeout"
: "connection_lost";
return {
issue: "outcome_unknown",
intentId: input.intentId,
idempotencyKey: key,
retryable: false,
reconcileAfterMs: 2_000,
reason,
};
}
}Le type outcome_unknown est déterminant. Il empêche une erreur de transport de devenir automatiquement une permission de répéter l’effet. Son champ retryable: false ne signifie pas que l’intention est abandonnée ; il signifie que la prochaine étape doit être une lecture de réconciliation, pas une nouvelle mutation aveugle.
Le timeout après paiement : l’échec qui n’en est peut-être pas un
Considérons une exécution concrète :
- le harness valide une intention de paiement ;
- il réserve la clé
charge:tenant:intent; - le prestataire débite la carte ;
- sa réponse traverse le réseau ;
- la connexion est interrompue avant que le harness ne reçoive le reçu.
L’agent voit un timeout. Le client, lui, a peut-être déjà été débité. Répéter avec une nouvelle clé crée potentiellement un second paiement. Déclarer l’opération échouée produit un état métier faux. Continuer le workflow comme si le paiement était confirmé manque de preuve.
Le harness place donc l’intention dans un état ambigu, puis interroge le prestataire avec la clé stable, l’identifiant de commande ou un identifiant de recherche prévu par le contrat. Trois résultats sont possibles :
- un paiement correspondant existe : le harness enregistre le reçu et valide l’intention ;
- aucun paiement n’existe et la fenêtre de cohérence est dépassée : une reprise avec la même clé peut être autorisée ;
- le prestataire reste indisponible ou incohérent : l’intention demeure ambiguë et peut être escaladée.
La réconciliation ne consiste pas à demander au modèle ce qu’il pense avoir fait. Elle compare l’état attendu à des preuves externes : reçu du prestataire, statut de ressource, journal transactionnel, message présent dans la boîte d’envoi ou identifiant attribué par l’API.
La politique de reprise dépend alors de la nature de l’opération :
| Situation observée | Effet potentiel | Politique du harness | Nouvelle clé ? |
|---|---|---|---|
| Arguments invalides ou JSON non conforme | Aucun appel effectué | Rejeter ; autoriser au plus une correction bornée si la politique le prévoit | Non |
| Lecture échouée avant réponse | Aucun effet attendu | Reprendre avec backoff, jitter et limite de tentatives | Sans objet |
Écriture idempotente ayant retourné 429 ou 503 | Effet possible selon le contrat | Reprendre avec la même clé après délai | Jamais |
| Timeout après envoi d’un paiement | Effet inconnu | Réconcilier avant toute nouvelle mutation | Jamais avant nouvelle intention |
| Même clé, arguments différents | Intention incohérente | Rejeter et demander une nouvelle intention explicite | Oui, seulement pour la nouvelle intention |
| API non idempotente, effet critique | Effet non déductible | Pas de reprise automatique ; vérification ou intervention humaine | Non |
| Résultat confirmé par preuve externe | Effet établi | Clore l’intention et retourner le résultat mémorisé | Non |
Une clé différente affirme une nouvelle intention. Elle ne doit pas servir à contourner une erreur de déduplication, un statut pending ou une réponse inconfortable.
Borner la boucle avant qu’elle ne s’emballe
Un runaway agent n’est pas toujours spectaculaire. Il peut simplement alterner recherche, reformulation et nouvel appel d’outil sans modifier l’état utile. Chaque étape paraît localement raisonnable ; l’ensemble n’a plus de condition de sortie.

Le harness doit posséder ses propres compteurs, indépendants du contexte conversationnel du modèle :
- nombre maximal d’itérations ;
- nombre maximal d’appels par outil et au total ;
- budget de tokens d’entrée et de sortie ;
- coût monétaire maximal par intention ;
- durée murale maximale ;
- nombre maximal de corrections de schéma ;
- nombre maximal de reprises réseau ;
- nombre maximal de délégations à d’autres agents ;
- seuil d’actions sensibles exigeant une approbation.
Ces budgets ne sont pas de simples protections financières. Ils définissent la sémantique d’arrêt. Une exécution doit pouvoir se terminer par validated, rejected, outcome_unknown, budget_exhausted ou cancelled. « Le modèle n’a plus rien écrit » n’est pas un état de workflow.
Un bon contrôleur mesure aussi le progrès. Après chaque étape, il compare une représentation compacte de l’état : nouvelles preuves obtenues, sous-objectifs résolus, erreurs éliminées, mutation confirmée. Si plusieurs itérations consomment des ressources sans changement observable, la boucle s’arrête avant d’épuiser son plafond global.
Choix d’architecture : on peut imposer, par exemple, un budget distinct aux lectures et aux mutations, réserver la dernière itération à la vérification et interdire toute nouvelle action sensible lorsque le temps restant ne permet plus de la réconcilier. Les valeurs exactes dépendent du risque, de la latence des services et du coût acceptable ; elles ne constituent pas des constantes universelles.
L’arrêt doit enfin être persistant. Si un worker redémarre, les compteurs, la clé d’intention et le dernier état connu doivent être restaurés. Sinon, chaque replay obtient un budget neuf et contourne mécaniquement la limite.
Observer une causalité, pas collectionner des logs
Une suite de lignes horodatées ne suffit pas à expliquer pourquoi un agent a débité un compte. Il faut relier l’intention initiale, la décision du modèle, la validation, l’appel réseau, la reprise éventuelle, la réconciliation et l’état final.
La spécification OpenTelemetry représente une trace comme un graphe orienté acyclique de spans liés par des relations parent-enfant. Un span peut porter un nom d’opération, des timestamps, des attributs, des événements, des liens et un statut. Cette structure convient à un agent distribué : l’inférence, le routeur, l’exécuteur d’outil, le prestataire externe et le réconciliateur deviennent des opérations causalement reliées.
La propagation de contexte OpenTelemetry permet de transporter Trace ID et Span ID entre processus et réseaux. Un appel d’outil effectué par un worker distinct peut ainsi rester attaché à la même exécution logique. Pour les files de messages, les workflows asynchrones ou les réconciliations tardives, les liens entre spans complètent la stricte relation parent-enfant.
Faits sourcés : OpenTelemetry définit la structure et la propagation de la causalité technique. La spécification ne définit pas, à elle seule, le modèle métier d’un agent ni sa politique d’idempotence.
Choix d’architecture : le harness peut ajouter des attributs tels que :
agent.intent.id,agent.run.idetagent.step.index;llm.model,llm.input_tokens,llm.output_tokensetllm.latency_ms;tool.name,tool.schema_versionettool.call_id;effect.idempotency_key_hash, jamais le secret ou le moyen de paiement ;validation.outcomeetvalidation.error_code;retry.attempt,retry.reasonetretry.prevented;reconciliation.statusetreconciliation.evidence_id;budget.remaining_calls,budget.remaining_tokensetbudget.remaining_ms;release.version,prompt.version,router.versionetpolicy.version.
Les erreurs de parsing, outils inexistants, refus de politique, timeouts et divergences entre résultat annoncé et état vérifié doivent être des événements structurés. Les prompts complets et les sorties brutes exigent davantage de prudence : ils peuvent contenir des données personnelles, des secrets ou un volume prohibitif. La traçabilité n’autorise pas une collecte indiscriminée.
Cette instrumentation rend les régressions silencieuses visibles. Un changement de modèle ou de prompt peut conserver un taux élevé de réponses apparemment correctes tout en augmentant le nombre d’appels, la latence, les corrections de JSON ou les résultats ambigus. Sans versionnement dans les traces, cette dérive est difficile à attribuer.
Mesurer l’intention validée plutôt que la réponse produite
Le taux de succès HTTP et la qualité textuelle ne mesurent pas la fiabilité d’un système qui agit. Les métriques doivent prendre l’intention comme unité de compte.
- Outcome verification rate : proportion des intentions déclarées réussies pour lesquelles une preuve externe ou transactionnelle confirme réellement l’effet.
- Ambiguous outcome rate : proportion des intentions terminant dans un état inconnu après expiration de leur fenêtre normale de réconciliation.
- Retries évités : nombre de mutations supplémentaires bloquées parce qu’une clé existait, qu’un résultat était déjà mémorisé ou qu’une réconciliation était exigée.
- Coût par intention validée : somme des coûts de modèles, outils, calcul et réconciliation divisée par le nombre d’intentions dont le résultat est confirmé.
- Boucles arrêtées par budget : nombre et proportion d’exécutions interrompues par limite de temps, tokens, appels, coût ou absence de progrès.
Ces métriques doivent être segmentées par outil, type d’effet, version de modèle, prompt, politique et release du harness. Une moyenne globale peut masquer un prestataire de paiement instable ou un outil dont le schéma provoque davantage de corrections.
Le drift mérite plusieurs lectures : évolution des distributions d’arguments, hausse des tools calls inexistants, augmentation des tokens par intention, dégradation de la latence de réconciliation ou baisse du taux de preuve. Une alerte utile indique la dimension qui dérive et la version à partir de laquelle elle a changé.
Les évaluations hors ligne restent nécessaires, mais elles doivent rejouer des traces réalistes : JSON tronqué, réponse tardive, duplication de message, timeout après effet, ancienne clé avec nouveaux paramètres, prestataire incohérent, budget presque épuisé. Une évaluation qui vérifie uniquement la réponse finale ne teste pas le système qui la rend sûre.
Les limites qu’un harness ne supprime pas
L’idempotence a une portée et une durée. Les enregistrements de clés ne peuvent pas toujours être conservés indéfiniment ; leur expiration doit correspondre à la durée pendant laquelle une requête tardive peut encore arriver et à la vie de la ressource concernée. Une clé réutilisée après expiration peut perdre sa protection.
L’atomicité complète n’est pas toujours possible entre deux systèmes indépendants. Si une API externe ne fournit ni clé d’idempotence ni recherche fiable, le harness ne peut pas fabriquer une garantie « exactement une fois ». Il peut seulement réduire le risque, suspendre les reprises et solliciter une intervention.
La réconciliation dépend de la qualité des preuves. Un système éventuellement cohérent peut répondre « absent » alors que l’effet existe déjà. Le contrôleur doit connaître les fenêtres de cohérence et éviter de transformer une lecture prématurée en autorisation de muter.
Les budgets peuvent interrompre une opération qui aurait réussi avec une étape supplémentaire. Ils imposent un compromis entre disponibilité, coût et risque. Pour un effet réversible et peu coûteux, une politique généreuse peut convenir ; pour un paiement ou une suppression irréversible, l’ambiguïté assumée vaut mieux qu’une reprise agressive.
L’observabilité peut elle-même échouer, coûter cher ou exposer des données. Les traces doivent être échantillonnées avec discernement, protégées par des règles de rétention et conçues pour ne pas devenir une dépendance bloquante de l’effet métier.
Enfin, le harness ne corrige pas une intention humaine ambiguë, une politique métier erronée ou un outil externe qui ment sur son propre état. Il rend ces limites explicites et auditables ; il ne les abolit pas.
La qualité du modèle reste importante pour choisir une action, interpréter un contexte et réparer une proposition rejetée. Mais la confiance opérationnelle vient d’un niveau différent : une intention stable, des contrats stricts, des effets dédupliqués, des boucles bornées, une causalité observable et un chemin de réconciliation. Un agent peut alors se tromper sans que chaque erreur devienne immédiatement un incident externe.
Sources et prolongements
- Post de divaagurlxw sur le harness engineering — point de départ sur le déplacement de l’enjeu, du modèle isolé vers son système d’exécution.
- Post d’adxtyahq sur les agents en production — prolongement sur la difficulté de coordonner outils, mémoire, routage, évaluations et validation.
- AWS Builders’ Library — Making retries safe with idempotent APIs — référence sur les identifiants fournis par l’appelant, les doublons, l’atomicité et les changements d’intention.
- AWS Durable Execution — Idempotency and retries — recommandations sur les effets critiques, les sémantiques de reprise et la stabilité des clés.
- OpenTelemetry Specification — Overview — définition des traces, spans, identifiants, timestamps, attributs, événements, liens et relations causales.
- OpenTelemetry — Context propagation — transport du contexte de trace entre services, processus et frontières réseau.