Tutoriel - Strategy, agents et skills
La stratégie délègue une demande de changement à un agent, qui dispose d'une skill déclarée. Celle-ci écrit réellement une entrée dans un fichier local via un tool gouverné. Le provider déterministe ne nécessite ni modèle ni appel payant.
1. Préparer le projet
Prérequis : distribution Touba, Node.js 24 et npm installés. Voir le guide d'installation du SDK. Ces commandes macOS/Linux installent l’archive livrée avec la CLI dans le projet :
mkdir -p change-review-demo
cd change-review-demo
mkdir -p handlers providers 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
node --input-type=module -e 'import {defineTool} from "@touba/sdk"; console.log(typeof defineTool);'
2. Déclarer la responsabilité et le flux
module examples.change_review
type ChangeRequest
summary is Text
type ChangeResult
summary is Text
tool ApplyChange
description "Append the requested change to the local change journal."
takes request as ChangeRequest
gives ChangeResult
requires filesystem_write
skill MakeChange
description "Apply one requested change through the authorized write tool."
takes request as ChangeRequest
gives ChangeResult
uses ApplyChange
do
ApplyChange
with request as request
into result
give result
agent Engineer
responsibility "Record the requested change through MakeChange, then return its observed result. Do not repeat a completed write for the same request unless the user explicitly requests another change."
takes request as ChangeRequest
gives ChangeResult
uses MakeChange
callable_by strategy RunChangeReview
strategy RunChangeReview
takes request as ChangeRequest
gives ChangeResult
uses Engineer
do
Engineer
with request as request
into result
give result
La stratégie appelle seulement l'agent. Elle ne rappelle pas la skill après la
délégation. Les entrées et la sortie de l'agent sont explicites. Un appelant
statique non autorisé est refusé à la compilation avec AgentCallerNotAllowed ;
la délégation dynamique est aussi contrôlée au runtime. Un refus de compilation
n'est pas une trace d'exécution.
3. Implémenter l'écriture réelle
import { appendFile } from "node:fs/promises";
import { createToolResult, defineTool, ToubaHandlerError } from "@touba/sdk";
await defineTool({
toolRef: "tool:examples.change_review/ApplyChange",
inputSignature: [{ name: "request", type_ref: "type:examples.change_review/ChangeRequest" }],
outputType: "type:examples.change_review/ChangeResult",
requiredGrants: ["filesystem_write"],
run: async ({ request, context }) => {
if (!context.effect) throw new ToubaHandlerError("EffectContextRequired", "A durable write context is required.");
await appendFile(new URL("../changes.ndjson", import.meta.url), JSON.stringify(request) + "\n");
return createToolResult({ summary: request.summary }, {
effectReceipt: context.createEffectReceipt({ outcome: "applied" }),
});
},
});
Cet ajout de ligne n'est pas idempotent : le contrat déclare donc
retry_mode: none. Une issue inconnue ne peut pas être rejouée comme sûre.
import { writeFileSync } from "node:fs";
writeFileSync("touba.manifest.json", JSON.stringify({
manifest_version: "touba-manifest-v1",
bindings: {
"tool:examples.change_review/ApplyChange": {
handler: { kind: "process", command: process.execPath,
args: ["handlers/apply-change.mjs"], cwd: "." },
grants: ["filesystem_write"], timeout_ms: 5000, max_payload_bytes: 65536,
effect_contract: { kind: "external_write", retry_mode: "none" },
},
},
}, null, 2) + "\n");
4. Terminer après observation du résultat
import { isDeepStrictEqual } from "node:util";
export function decideAgentCapability(request) {
const target = "skill:examples.change_review/MakeChange";
const args = { request: request.inputs.request };
const completed = request.observations.findLast(item =>
item.target_ref === target && isDeepStrictEqual(item.arguments, args)
&& typeof item.value?.summary === "string");
if (completed) return { action: "answer_directly", result: completed.value,
reason: "The requested change has been recorded." };
if (request.mode === "finalization") return { action: "cannot_complete",
reason: "No completed write is available within the allowed budget." };
if (request.mode === "summary") return { action: "request_capabilities",
reason: "The requested change requires the authorized capability." };
return { action: "select_capability", target_ref: target, arguments: args,
reason: "Record the requested change through the declared skill." };
}
Le provider reconnaît l'appel terminé et restitue sa valeur observée. C'est une politique applicative, pas une interdiction runtime d'utiliser deux fois une capacité. Un autre appel reste possible tant que droits et budgets l'autorisent.
5. Exécuter et vérifier
node configure.mjs
touba compile source.tb --out change.air.json
touba run change.air.json --entry strategy:examples.change_review/RunChangeReview \
--agent-decision-provider providers/engineer.provider.mjs --max-agent-steps 4 \
--inputs-json '{"request":{"summary":"Correct the report title"}}' \
--manifest touba.manifest.json --effect-journal changes.effect-journal.sqlite \
--output json --trace-out trace.json
node --input-type=module -e 'import assert from "node:assert/strict"; import {readFileSync} from "node:fs"; const rows=readFileSync("changes.ndjson","utf8").trim().split("\n"); assert.equal(rows.length,1); assert.equal(JSON.parse(rows[0]).summary,"Correct the report title"); console.log("ONE_CHANGE_RECORDED");'
Résultat attendu : status: success, le résumé demandé,
ONE_CHANGE_RECORDED et une seule entrée appliquée dans le journal d'effets.
La trace expose sélection et exécution, sans recopier la requête brute. Enlever
l'option de journal refuse l'écriture avant dispatch. Retirer
filesystem_write du manifest la refuse également avant le handler.
Sans provider, un agent ayant exactement une capacité déclarée peut la choisir
automatiquement (uses_single_capability). Les entrées doivent toujours
respecter l'interface explicite. Avec plusieurs capacités, fournissez un
provider. Une stratégie sans gives peut exposer le dernier résultat runtime ;
gives et give explicites rendent son interface métier plus claire.