Skip to main content
Version: 1.0.0

Tutorial - Strategy, agents and skills

The strategy delegates a change request to an agent, which can use one declared skill. The skill appends a real record to a local file through a governed tool. No model service or paid call is needed for this deterministic provider.

1. Prepare the project​

Install the Touba distribution, Node.js 24 and npm first. See the SDK installation guide. These macOS/Linux commands install the SDK archive shipped with the CLI, without a workspace link:

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. Declare the responsibility and flow​

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

The strategy calls only the agent. It does not repeat the skill after delegation. Both agent inputs and output are explicit. The source compiler refuses a known unauthorized caller with AgentCallerNotAllowed; the runtime also checks dynamic delegation before execution. A compilation refusal is not a runtime trace.

3. Implement the actual write​

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" }),
});
},
});

This append operation is not idempotent: its contract honestly declares retry_mode: none. An unknown outcome cannot be retried as if it were safe.

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. Finish after observing the result​

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." };
}

The provider matches the completed invocation and returns its observed value. This is application policy, not a runtime ban on using a capability twice. Another authorized call remains possible while authority and budgets permit it.

5. Execute and verify​

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");'

Expected: status: success, the requested summary, ONE_CHANGE_RECORDED, and one applied entry in the effect journal. The trace records selection and execution without copying the raw request. Removing the journal option refuses the write before dispatch. Removing filesystem_write from the manifest also refuses it before the handler.

Without a provider, an agent with exactly one declared capability may select it automatically (uses_single_capability). Inputs must still match the explicit interface. With several capabilities, provide a decision provider. A strategy without gives can expose the last runtime result, but explicit gives and give make a reusable business interface clear.