Aller au contenu principal
Version: 1.0.0

Tutoriel - Mémoire scopée avec agent

Lire un véritable fichier de connaissances par un tool typé. Distinguer mémoire logique, scopes source, grants hôte et stockage réel. Prérequis : Touba, Node.js 24 et npm. Aucun service modèle ni base externe n'est nécessaire.

1. Préparer le projet isolé​

Dans un terminal macOS/Linux, installer le SDK distribué :

Terminal
mkdir -p scoped-memory-demo
cd scoped-memory-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
knowledge.json
{
"architecture": "Project Atlas separates the governed runtime from its storage adapters.",
"deployment": "The staging service is deployed separately from production."
}

2. Déclarer la mémoire et le droit de lecture​

memory.tb
module examples.memory_agent_read

memory ProjectKnowledge

type KnowledgeQuery
text is Text

type KnowledgeResult
summary is Text

tool ReadProjectKnowledge
description "Read the project knowledge file for an exact topic."
uses memory ProjectKnowledge read
takes query as KnowledgeQuery
gives KnowledgeResult
requires memory_read, filesystem_read

skill UseProjectKnowledge
description "Read an authorized topic from project knowledge."
takes query as KnowledgeQuery
gives KnowledgeResult
uses ReadProjectKnowledge
memory ProjectKnowledge read
do
ReadProjectKnowledge
with query as query
into result
give result

agent Researcher
description "Answer the requested topic using project knowledge."
responsibility "Use the authorized knowledge skill and return its observed KnowledgeResult."
takes query as KnowledgeQuery
gives KnowledgeResult
memory ProjectKnowledge read
uses UseProjectKnowledge
callable_by strategy RunKnowledgeReview

strategy RunKnowledgeReview
takes query as KnowledgeQuery
gives KnowledgeResult
uses Researcher
do
Researcher
with query as query
into result
give result

L'agent et la skill déclarent leurs scopes de lecture. Le tool déclare son accès à la ressource et ses grants requis. memory ne stocke et ne lit aucune donnée par elle-même.

3. Raccorder le handler fichier​

handlers/read.mjs
import { readFile } from "node:fs/promises";
import { defineTool, ToubaHandlerError } from "@touba/sdk";
await defineTool({
toolRef: "tool:examples.memory_agent_read/ReadProjectKnowledge",
inputSignature: [{ name: "query", type_ref: "type:examples.memory_agent_read/KnowledgeQuery" }],
outputType: "type:examples.memory_agent_read/KnowledgeResult",
requiredGrants: ["memory_read", "filesystem_read"],
run: async ({ request: query }) => {
const knowledge = JSON.parse(await readFile("knowledge.json", "utf8"));
if (!Object.hasOwn(knowledge, query.text) || typeof knowledge[query.text] !== "string") {
throw new ToubaHandlerError("UnknownTopic", "The requested topic is absent from project knowledge.");
}
return { summary: knowledge[query.text] };
},
});
configure.mjs
import { writeFileSync } from "node:fs";
const manifest = { manifest_version: "touba-manifest-v1", bindings: {
"tool:examples.memory_agent_read/ReadProjectKnowledge": {
handler: { kind: "process", command: process.execPath, args: ["handlers/read.mjs"], cwd: "." },
grants: ["memory_read", "filesystem_read"],
memories: [{ ref: "memory:examples.memory_agent_read/ProjectKnowledge", access: "read" }],
effect_contract: { kind: "none" }, timeout_ms: 10000, max_payload_bytes: 65536,
} },
};
writeFileSync("touba.manifest.json", JSON.stringify(manifest, null, 2) + "\n");
manifest.bindings["tool:examples.memory_agent_read/ReadProjectKnowledge"].memories = [];
writeFileSync("denied.manifest.json", JSON.stringify(manifest, null, 2) + "\n");

memories est un tableau de { ref, access }, pas un objet indexé par nom de ressource. Le second manifest omet volontairement l'autorisation mémoire. Le chemin fichier appartient ici à l'adaptateur ; les resource bindings hôte montrent comment externaliser la configuration du backend.

4. Exécuter et vérifier les deux chemins​

Le provider déterministe corrèle chaque observation à ses arguments. Il n'interdit pas la réutilisation d'une capacité et ne devient pas la source d'autorité.

providers/researcher.mjs
import { isDeepStrictEqual } from "node:util";
export function decideAgentCapability(request) {
const target = "skill:examples.memory_agent_read/UseProjectKnowledge";
const args = { query: request.inputs.query };
const observed = request.observations?.findLast(item => item.target_ref === target
&& isDeepStrictEqual(item.arguments, args) && typeof item.value?.summary === "string");
if (observed) return { action: "answer_directly", source: "custom_provider",
reason: "The requested result is available in the matching observation.", result: observed.value };
if (request.mode === "finalization") return { action: "cannot_complete", source: "custom_provider",
reason: "No matching result is available within the execution budget." };
if (request.mode === "summary") return { action: "request_capabilities", source: "custom_provider",
reason: "The requested result requires the authorized capability." };
if (!request.capability_refs.includes(target)) throw new Error("Required capability unavailable");
return { target_ref: target, arguments: args, source: "custom_provider",
reason: "The authorized capability produces the requested result." };
}
verify.mjs
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
const args = ["run", "memory.air.json", "--entry", "strategy:examples.memory_agent_read/RunKnowledgeReview",
"--agent-decision-provider", "providers/researcher.mjs", "--inputs-json", '{"query":{"text":"architecture"}}',
"--output", "json"];
for (const [manifest, expectedExit] of [["touba.manifest.json", 0], ["denied.manifest.json", 1]]) {
const child = spawnSync("touba", [...args, "--manifest", manifest], { encoding: "utf8" });
assert.equal(child.status, expectedExit, child.stderr);
const run = JSON.parse(child.stdout);
const authorized = run.trace.events.filter(event => event.kind === "tool_binding_authorized");
if (expectedExit === 0) {
assert.equal(run.status, "success");
assert.equal(run.result.summary, JSON.parse(readFileSync("knowledge.json", "utf8")).architecture);
assert.ok(run.trace.events.some(event => event.kind === "memory_access_authorized"));
assert.ok(!JSON.stringify(run.trace).includes(run.result.summary));
assert.equal(authorized.length, 1);
console.log(run.result.summary);
} else {
assert.equal(run.status, "failure");
assert.match(run.reason, /Memory/);
assert.equal(authorized.length, 0);
}
}
console.log("SCOPED_MEMORY_READ_AND_REFUSAL_OK");
Terminal
node configure.mjs
touba compile memory.tb --out memory.air.json
node verify.mjs

Résultat attendu : SCOPED_MEMORY_READ_AND_REFUSAL_OK. Le résultat autorisé correspond au contenu réel du fichier. Le run refusé termine avec le code 1 avant dispatch. La trace décrit l'accès autorisé, pas le texte stocké.

Un grant n'est pas une sandbox du système de fichiers. L'hôte doit confiner le code handler de confiance au stockage voulu. PostgreSQL, fichiers et bases vectorielles restent des choix hôtes, pas des moteurs mémoire intégrés à Touba.