Aller au contenu principal
Version: 1.0.0

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.

Terminal
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​

cancel-demo.tb
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
handlers/wait.mjs
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";
},
});
configure.mjs
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.

exercise.mjs
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");
Terminal
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.