Compensations explicites et bornées
Une compensation demande au runtime d'appliquer l'opération inverse déclarée pour un effet externe précis. Elle est explicite : un échec, un fallback, une annulation ou un rollback de déploiement ne la déclenche jamais.
Quand l'utiliser
Utilisez cette capacité quand un service externe a confirmé une écriture et que le package sait fournir une inverse métier précise, par exemple libérer une réservation précédemment créée.
L'effet original reste applied dans le journal. L'inverse possède son propre
run, son propre effet, ses budgets, ses secrets et son autorité. Le résultat
peut être :
completed: l'inverse déclarée a été appliquée ;partial: l'inverse a été appliquée partiellement ;unavailable: aucune inverse ou aucun reçu exploitable n'est disponible ;failed: le handler prouve que l'inverse n'a pas été appliquée ;unknown: l'issue ne peut pas être établie de façon sûre.
Déclarer l'inverse
Le binding de l'écriture originale utilise un manifest V1 et désigne une stratégie existante :
{
"manifest_version": "touba-manifest-v1",
"bindings": {
"tool:reservations/CreateReservation": {
"handler": {
"kind": "process",
"command": "node",
"args": ["handlers/create-reservation.mjs"]
},
"grants": ["network"],
"secrets": ["RESERVATION_API_TOKEN"],
"timeout_ms": 5000,
"max_payload_bytes": 65536,
"effect_contract": {
"kind": "external_write",
"retry_mode": "idempotency_key",
"compensation": {
"entry_ref": "strategy:reservations/ReleaseReservation"
}
}
}
}
}
La stratégie inverse reste volontairement petite : des bind purs, au plus
une approbation humaine, exactement un appel de tool externe, puis give ou
stop. Branches, boucles, fallback, appels imbriqués, interactions, deuxième
effet et compensation récursive sont refusés.
Implémenter le handler inverse
Le SDK expose le contexte de compensation uniquement pour l'appel inverse :
import { createToolResult, defineTool } from "@touba/sdk";
await defineTool({
toolRef: "tool:reservations/ReleaseReservation",
inputSignature: [
{ name: "reservation_id", type_ref: "builtin:Text" },
],
outputType: "builtin:Text",
run: async ({ request: reservation_id, context }) => {
const response = await releaseReservation({
reservationId: reservation_id,
originalReceipt: context.compensation.original_effect_receipt,
idempotencyKey: context.effect.effect_id,
});
return createToolResult(response.message, {
effectReceipt: context.createEffectReceipt({
outcome: "applied",
receipt_ref: response.receiptRef,
}),
compensationReceipt: context.createCompensationReceipt({
result: response.complete ? "completed" : "partial",
}),
});
},
});
Cet extrait suppose un adaptateur métier releaseReservation fourni par
votre package ; ce n'est pas un helper du SDK. L'appel inverse utilise
operation: compensate dans le protocole handler V1.
Le reçu original est une donnée de contrôle. Ne le recopiez pas dans les arguments métier, les logs ou les traces.
Création, reprise et approbation
L'hôte appelle séparément la création et la reprise. Chaque commande passe par une autorité de ciblage avant la lecture du journal. L'autorité est ensuite revalidée pour progresser et immédiatement avant un nouveau spawn.
Une reprise fournit la révision et le digest courants du record. Un rejeu exact déjà traité est une lecture pure : il ne relance pas le handler et ne produit pas de nouvelle trace. Une approbation expirée peut être renouvelée pour le même sujet et la même référence ; elle n'est pas héritée du run original.
Limites
Le store SQLite fourni vérifie atomiquement révision et empreinte, y compris entre plusieurs processus locaux. L'hôte coordonne les exécutions et conserve les sauvegardes. Ce store ne fournit pas de lease multi-hôte ni d'historique inviolable. Les anciens fichiers JSON utilisent la migration explicite présentée dans Stockage et conservation.
Une compensation terminée prouve l'application de l'inverse déclarée, pas la
restauration parfaite du monde extérieur. Cette capacité n'est ni une saga,
ni une transaction distribuée, ni une garantie exactly-once.