Aller au contenu principal
Version: 1.0.0

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 :

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

source.tb
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​

handlers/apply-change.mjs
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.

configure.mjs
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​

providers/engineer.provider.mjs
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​

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