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