Devolver una carpeta a como estaba
Una instantánea es una copia de todos los archivos de una carpeta, tomada antes de que actúe un agente. Una reversión devuelve la carpeta a esa copia.
Importa porque incluso un agente confinado puede estropear los archivos que sí puede tocar. Con una instantánea puedes decir: devuélvelo todo, exactamente.
Al final de esta lección
Nota
Puedo devolver una carpeta a como estaba antes de que actuara un agente.
Palabras clave
| Palabra | Qué significa |
|---|---|
| Instantánea | una copia de cada archivo y su texto |
| Diferencias | la lista de lo que cambió entre dos estados |
| Restaurar | devolver los archivos desde una instantánea |
| Reversión | una restauración de toda la carpeta |
| Archivo añadido | un archivo que no estaba en la instantánea |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Una instantánea guarda cada archivo
Antes de que empiece el agente, lee cada archivo de la carpeta y guarda su ruta y su texto. Guárdalo donde el agente no pueda llegar.
Compara por ruta. Cuando el agente termina, un archivo está añadido si es nuevo, quitado si ya no está y cambiado si su texto es distinto.
Ejemplo resuelto: tres ediciones
| Edición del agente | En las diferencias | Para devolverlo |
|---|---|---|
| Escribió src/app.js con texto nuevo | Cambiado | Escribir el texto antiguo |
| Borró notas.txt | Quitado | Escribir el archivo otra vez |
| Creó nuevo.txt | Añadido | Borrarlo |
| Escribió un archivo con su texto antiguo | Nada | Nada que hacer |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.
Un error frecuente
Advertencia
Mucha gente cree que restaurar es solo reescribir los archivos cambiados. También hay que borrar los archivos que añadió el agente, o la carpeta no queda como estaba. Después comprueba: tras restaurar, las diferencias deben estar vacías.
Demuestra que la restauración funcionó
Una restauración que no compruebas es una esperanza. Tras restaurar, toma otra instantánea y compárala con la primera. Si no hay diferencias, la carpeta está exactamente como estaba.
Esto compara solo archivos. Una carpeta vacía que creó el agente no aparece en una lista de archivos, así que di qué no cubre tu instantánea.
Escribe el ayudante del modelo, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// Los tokens de una respuesta, o null si no trae ninguno.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el ayudante del modelo, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, 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(\"estado \" + res.status);' ' const d = await res.json();' ' return { text: d.choices[0].message.content, inTokens: count(d, \"prompt_tokens\"), outTokens: count(d, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Muestra grabada: ningún modelo respondió (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el guion de reversión, parte 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const fs = require(\"fs\");' 'const path = require(\"path\");' 'const root = path.join(process.cwd(), \"snap-lab\");' 'function put(rel, text) {' ' const f = path.join(root, rel);' ' fs.mkdirSync(path.dirname(f), { recursive: true });' ' fs.writeFileSync(f, text);' '}' 'function names(dir) {' ' let out = [];' ' for (const n of fs.readdirSync(dir).sort()) {' ' const f = path.join(dir, n);' ' out = fs.statSync(f).isDirectory() ? out.concat(names(f)) : out.concat([f]);' ' }' ' return out;' '}' > rollback.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el guion de reversión, parte 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function take() {' ' const s = {};' ' names(root).forEach(function (f) { s[path.relative(root, f)] = fs.readFileSync(f, \"utf8\"); });' ' return s;' '}' 'function diff(a, b) {' ' const d = { added: [], changed: [], removed: [] };' ' Object.keys(b).forEach(function (k) { if (a[k] === undefined) d.added.push(k); else if (a[k] < b[k] || a[k] > b[k]) d.changed.push(k); });' ' Object.keys(a).forEach(function (k) { if (b[k] === undefined) d.removed.push(k); });' ' return d;' '}' 'function restore(snap) {' ' const now = take();' ' Object.keys(now).forEach(function (k) { if (snap[k] === undefined) fs.rmSync(path.join(root, k)); });' ' Object.keys(snap).forEach(function (k) { put(k, snap[k]); });' '}' >> rollback.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el guion de reversión, parte 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'fs.rmSync(root, { recursive: true, force: true });' 'put(\"src/app.js\", \"let x = 1;\");' 'put(\"src/util.js\", \"export {};\");' 'put(\"notas.txt\", \"pendiente\");' >> rollback.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Escribe el guion de reversión, parte 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const snap = take();' ' console.log(\"instantánea: \" + Object.keys(snap).join(\" \"));' ' put(\"src/app.js\", \"let x = 2;\");' ' fs.rmSync(path.join(root, \"notas.txt\"));' ' put(\"nuevo.txt\", \"hola\");' ' console.log(\"cambios: \" + JSON.stringify(diff(snap, take())));' ' restore(snap);' ' console.log(\"igual tras restaurar: \" + (JSON.stringify(take()) === JSON.stringify(snap)));' ' const q = \"En una frase corta: ¿por qué conviene tomar una instantánea antes de que un agente edite archivos?\";' ' const sample = \"Si las ediciones salen mal, puedes devolver cada archivo exactamente a como estaba.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"el modelo dijo: \" + reply.text);' '}' 'main();' >> rollback.js"Se muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Ejecuta la reversión
docker exec -w /home/node/project agent node rollback.jsSe muestra aquí y no se ejecuta. Crea una cuenta gratuita para pulsar Ejecutar y escribirlo en el terminal de tu propio laboratorio.
Lo que imprime el Run
| Línea | Qué muestra |
|---|---|
| instantánea: notas.txt src/app.js src/util.js | Tres archivos reales en snap-lab |
| cambios: {"added":["nuevo.txt"],"changed":["src/app.js"],"removed":["notas.txt"]} | Lo que hizo el agente de pega |
| igual tras restaurar: true | La carpeta vuelve a ser igual a la instantánea |
| el modelo dijo: ... | Una frase. Las palabras cambian en cada ejecución |
Las celdas que rellenan los estudiantes quedan en blanco en esta vista previa.