Putting a folder back as it was
A snapshot is a copy of every file in a folder, taken before an agent acts. A rollback puts the folder back to that copy.
That matters because even a confined agent can ruin the files it is allowed to touch. With a snapshot you can say: put it all back, exactly.
By the end of this lesson
Note
I can put a folder back as it was before an agent acted.
Key words
| Word | What it means |
|---|---|
| Snapshot | a copy of every file and its text |
| Diff | the list of what changed between two states |
| Restore | to put files back from a snapshot |
| Rollback | a restore of the whole folder |
| Added file | a file that was not in the snapshot |
The cells students fill in are left blank in this preview.
A snapshot holds every file
Before the agent starts, read every file in the folder and keep its path and its text. Keep it somewhere the agent cannot reach.
Compare by path. After the agent acts, a file is added if it is new, removed if it has gone, and changed if its text is different.
Worked example: three edits
| Edit the agent made | In the diff | To put it back |
|---|---|---|
| Wrote src/app.js with new text | Changed | Write the old text |
| Deleted notes.txt | Removed | Write the file again |
| Created new.txt | Added | Delete it |
| Wrote a file back to its old text | Nothing | Nothing to do |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think a restore only has to rewrite the files that changed. It must also delete the files the agent added, or the folder is not as it was. Then check: after the restore, the diff must be empty.
Prove the restore worked
A restore that you do not check is a hope. After restoring, take a new snapshot and compare it with the first. If the diff is empty, the folder is exactly as it was.
This compares files only. An empty folder the agent made is not in a list of files, so say what your snapshot does not cover.
Write the model helper, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the model helper, part 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(\"status \" + res.status);' ' const data = await res.json();' ' const text = data.choices[0].message.content;' ' return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' ' } catch (error) {' ' console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rollback script, part 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"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rollback script, part 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"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the rollback script, part 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(\"notes.txt\", \"todo\");' 'async function main() {' ' const snap = take();' ' console.log(\"snapshot: \" + Object.keys(snap).join(\" \"));' ' put(\"src/app.js\", \"let x = 2;\");' ' fs.rmSync(path.join(root, \"notes.txt\"));' ' put(\"new.txt\", \"hello\");' ' console.log(\"changes: \" + JSON.stringify(diff(snap, take())));' ' restore(snap);' ' console.log(\"same after restore: \" + (JSON.stringify(take()) === JSON.stringify(snap)));' ' const q = \"In one short sentence, why take a snapshot before an agent edits files?\";' ' const sample = \"If the edits go wrong you can put every file back exactly as it was.\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 60);' ' console.log(\"model said: \" + reply.text);' '}' 'main();' >> rollback.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run the rollback
docker exec -w /home/node/project agent node rollback.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the Run prints
| Line | What it shows |
|---|---|
| snapshot: notes.txt src/app.js src/util.js | Three real files in snap-lab |
| changes: {"added":["new.txt"],"changed":["src/app.js"],"removed":["notes.txt"]} | What the stand-in agent did |
| same after restore: true | The folder equals the snapshot again |
| model said: ... | A sentence. The words differ each run |
The cells students fill in are left blank in this preview.