Remettre un dossier comme il était
Un instantané est une copie de chaque fichier d’un dossier, prise avant qu’un agent agisse. Un retour arrière remet le dossier dans l’état de cette copie.
C’est utile, car même un agent confiné peut abîmer les fichiers qu’il a le droit de toucher. Avec un instantané, vous pouvez dire : remettez tout, exactement.
À la fin de cette leçon
Remarque
Je peux remettre un dossier comme il était avant l’action d’un agent.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| Instantané | une copie de chaque fichier et de son texte |
| Différence | la liste de ce qui a changé entre deux états |
| Restaurer | remettre des fichiers à partir d’un instantané |
| Retour arrière | une restauration de tout le dossier |
| Fichier ajouté | un fichier qui n’était pas dans l’instantané |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un instantané contient chaque fichier
Avant que l’agent commence, lisez chaque fichier du dossier et gardez son chemin et son texte. Gardez la copie là où l’agent ne peut pas aller.
Comparez par chemin. Après l’action, un fichier est ajouté s’il est nouveau, supprimé s’il a disparu, et modifié si son texte est différent.
Exemple détaillé : trois modifications
| Modification de l’agent | Dans la différence | Pour le remettre |
|---|---|---|
| A écrit src/app.js avec un nouveau texte | Modifié | Écrire l’ancien texte |
| A supprimé notes.txt | Supprimé | Écrire de nouveau le fichier |
| A créé nouveau.txt | Ajouté | Le supprimer |
| A réécrit un fichier avec son ancien texte | Rien | Rien à faire |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Une erreur courante
Attention
Beaucoup pensent qu’une restauration n’a qu’à réécrire les fichiers modifiés. Elle doit aussi supprimer les fichiers que l’agent a ajoutés, sinon le dossier n’est pas comme avant. Puis vérifiez : après la restauration, la différence doit être vide.
Prouver que la restauration a marché
Une restauration que vous ne vérifiez pas est un espoir. Après avoir restauré, prenez un nouvel instantané et comparez-le au premier. Si la différence est vide, le dossier est exactement comme avant.
Cela compare les fichiers seulement. Un dossier vide créé par l’agent n’est pas dans une liste de fichiers : dites ce que votre instantané ne couvre pas.
Écrire l’assistant du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Les jetons d’une réponse, ou null si elle n’en donne pas.' 'function compter(donnees, nom) {' ' return donnees.usage && typeof donnees.usage[nom] === \"number\" ? donnees.usage[nom] : null;' '}' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’assistant du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' const d = await res.json();' ' return { texte: d.choices[0].message.content, entree: compter(d, \"prompt_tokens\"), sortie: compter(d, \"completion_tokens\"), enregistre: false };' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' return { texte: echantillon, entree: null, sortie: null, enregistre: true };' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de retour arrière, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' 'const fs = require(\"fs\");' 'const path = require(\"path\");' 'const racine = path.join(process.cwd(), \"labo-retour\");' 'function poser(rel, texte) {' ' const f = path.join(racine, rel);' ' fs.mkdirSync(path.dirname(f), { recursive: true });' ' fs.writeFileSync(f, texte);' '}' 'function noms(dossier) {' ' let sortie = [];' ' for (const n of fs.readdirSync(dossier).sort()) {' ' const f = path.join(dossier, n);' ' sortie = fs.statSync(f).isDirectory() ? sortie.concat(noms(f)) : sortie.concat([f]);' ' }' ' return sortie;' '}' > retour.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de retour arrière, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function prendre() {' ' const s = {};' ' noms(racine).forEach(function (f) { s[path.relative(racine, f)] = fs.readFileSync(f, \"utf8\"); });' ' return s;' '}' 'function difference(a, b) {' ' const d = { ajoutes: [], modifies: [], supprimes: [] };' ' Object.keys(b).forEach(function (k) { if (a[k] === undefined) d.ajoutes.push(k); else if (a[k] < b[k] || a[k] > b[k]) d.modifies.push(k); });' ' Object.keys(a).forEach(function (k) { if (b[k] === undefined) d.supprimes.push(k); });' ' return d;' '}' 'function restaurer(copie) {' ' const actuel = prendre();' ' Object.keys(actuel).forEach(function (k) { if (copie[k] === undefined) fs.rmSync(path.join(racine, k)); });' ' Object.keys(copie).forEach(function (k) { poser(k, copie[k]); });' '}' >> retour.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de retour arrière, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'fs.rmSync(racine, { recursive: true, force: true });' 'poser(\"src/app.js\", \"let x = 1;\");' 'poser(\"src/util.js\", \"export {};\");' 'poser(\"notes.txt\", \"a faire\");' >> retour.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le script de retour arrière, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const copie = prendre();' ' console.log(\"instantané : \" + Object.keys(copie).join(\" \"));' ' poser(\"src/app.js\", \"let x = 2;\");' ' fs.rmSync(path.join(racine, \"notes.txt\"));' ' poser(\"nouveau.txt\", \"bonjour\");' ' console.log(\"changements : \" + JSON.stringify(difference(copie, prendre())));' ' restaurer(copie);' ' console.log(\"identique après restauration : \" + (JSON.stringify(prendre()) === JSON.stringify(copie)));' ' const q = \"En une courte phrase, pourquoi prendre un instantané avant qu’un agent modifie des fichiers ?\";' ' const echantillon = \"Si les modifications tournent mal, on peut remettre chaque fichier exactement comme il était.\";' ' const r = await demander([{ role: \"user\", content: q }], echantillon, 60);' ' console.log(\"le modèle dit : \" + r.texte);' '}' 'main();' >> retour.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Lancer le retour arrière
docker exec -w /home/node/project agent node retour.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Ce que le Run affiche
| Ligne | Ce qu’elle montre |
|---|---|
| instantané : notes.txt src/app.js src/util.js | Trois vrais fichiers dans labo-retour |
| changements : {"ajoutes":["nouveau.txt"],"modifies":["src/app.js"],"supprimes":["notes.txt"]} | Ce qu’a fait l’agent joué par le script |
| identique après restauration : true | Le dossier égale de nouveau l’instantané |
| le modèle dit : ... | Une phrase. Les mots changent à chaque exécution |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.