Annulation, kill switch et timeout de run
Distinguer un succès, une interaction utilisateur en attente et une annulation contrôlée. Le timeout est coopératif : ce programme attend réellement deux secondes dans un handler, puis refuse le prochain dispatch après un budget de run d’une seconde. Ce n’est pas un arrêt forcé du processus ni un rollback d’effet acquis.
Prérequis : distribution Touba, Node.js 24 et npm. Terminal macOS/Linux.
mkdir -p cancel-demo
cd cancel-demo
mkdir -p handlers vendor
npm init -y
node --input-type=module -e 'import {execFileSync} from "node:child_process"; import {copyFileSync} from "node:fs"; const v=JSON.parse(execFileSync("touba",["version","--json"],{encoding:"utf8"})); copyFileSync(v.distribution.sdk_archive,"vendor/touba-sdk.tgz");'
npm install --offline --ignore-scripts --save-exact ./vendor/touba-sdk.tgz
Programme et handler
module examples.cancel_demo
tool Wait
description "Wait locally for the specified number of milliseconds without writing external state."
takes milliseconds as Int
gives Text
strategy RunCancelDemo
gives Text
do
show
with message as "run started"
ask
with prompt as "Continue?"
into answer
show
with message as "answer received: " ++ answer
give answer
strategy RunTimeoutDemo
gives Text
uses Wait
do
Wait
with milliseconds as 2000
into first
Wait
with milliseconds as 1
into second
give second
import { setTimeout } from "node:timers/promises";
import { defineTool } from "@touba/sdk";
await defineTool({ toolRef: "tool:examples.cancel_demo/Wait",
inputSignature: [{ name: "milliseconds", type_ref: "builtin:Int" }], outputType: "builtin:Text",
requiredGrants: [], run: async ({ request }) => {
await setTimeout(request);
return "wait completed";
},
});
import { writeFileSync } from "node:fs";
writeFileSync("touba.manifest.json", JSON.stringify({ manifest_version: "touba-manifest-v1", bindings: {
"tool:examples.cancel_demo/Wait": {
handler: { kind: "process", command: process.execPath, args: ["handlers/wait.mjs"], cwd: "." },
grants: [], secrets: [], effect_contract: { kind: "none" },
timeout_ms: 10000, max_payload_bytes: 65536,
},
} }, null, 2));
Vérifier les résultats
Le script récupère le véritable interaction_ref dans le résultat en attente.
Le nom de variable answer n’est pas l’identifiant d’interaction. Une pause
retourne le code 3 ; annulation, kill switch et timeout retournent le code 5.
Chaque résultat est conservé séparément avec sa trace. Aucune réponse n’est
inventée lors de l’annulation.
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { writeFileSync } from "node:fs";
function run(name, extra, exitCode, reason, entry = "RunCancelDemo") {
const child = spawnSync("touba", ["run", "cancel-demo.air.json", "--entry",
"strategy:examples.cancel_demo/" + entry, "--manifest", "touba.manifest.json", "--output", "json", ...extra],
{ encoding: "utf8" });
assert.equal(child.status, exitCode, child.stderr || child.stdout);
const result = JSON.parse(child.stdout);
assert.equal(result.reason, reason);
writeFileSync(name + ".result.json", JSON.stringify(result, null, 2));
return result;
}
const pending = run("pending", [], 3, "user_input_pending");
assert.equal(pending.status, "stopped");
const ref = pending.pending_interaction.interaction_ref;
assert.equal(typeof ref, "string");
const complete = run("complete", ["--user-inputs-json", JSON.stringify({ [ref]: "yes" })], 0, "completed");
assert.equal(complete.result, "yes");
for (const [name, flag, reason] of [
["before", "--cancel-before-dispatch", "cancelled"],
["pause", "--cancel-on-pause", "cancelled"],
["kill", "--kill-switch-enabled", "kill_switch"],
]) {
const result = run(name, [flag], 5, reason);
assert.equal(result.status, "cancelled");
assert.equal(result.result, null);
assert.ok(!JSON.stringify(result.messages).includes("answer received:"));
assert.ok(result.trace.events.some(event => event.kind === "run_cancelled"));
}
const timeout = run("timeout", ["--run-timeout-ms", "1000"], 5, "timeout", "RunTimeoutDemo");
assert.equal(timeout.status, "cancelled");
assert.ok(timeout.trace.events.some(event => event.kind === "run_timeout_exceeded"));
assert.ok(timeout.trace.events.some(event => event.kind === "dispatch_refused_cancelled"));
console.log("PAUSE_ANSWER_CANCEL_KILL_AND_TIMEOUT_OK");
node configure.mjs
touba compile cancel-demo.tb --out cancel-demo.air.json
node exercise.mjs
Sortie attendue : PAUSE_ANSWER_CANCEL_KILL_AND_TIMEOUT_OK. L’annulation ne
fournit ni reprise durable ni coordination distribuée du kill switch ; ces
responsabilités relèvent de mécanismes d’exécution et de l’hôte distincts.