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é :
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
{
"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
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
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] };
},
});
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é.
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." };
}
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");
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.