Make every change reversible
Undo for an agent means recording what every write replaced, so the files can be put back to any earlier step. Agents make mistakes at step seven that only show at step nine.
With a change log you rewind to step six. Without one you read a diff and guess.
By the end of this lesson
Note
I can record every change a run makes, so that I can rewind the files to any step.
Key words
| Word | What it means |
|---|---|
| Snapshot | a copy of a file before it changes |
| Change log | the list of every change, in order |
| Step | one numbered action of a run |
| Undo | putting a file back as it was |
| Rewind | undoing every change after a chosen step |
| Before text | what a file held before a write |
The cells students fill in are left blank in this preview.
Undo is not resume
A checkpoint saves a run so it can resume after a stop. That moves forward. Undo moves the files backward to how they were.
They keep different records. A checkpoint holds the agent's state. A change log holds what each write replaced.
Log the before text
Before every write or delete, record the step number, the path and what the file held. If the file did not exist, record that too, as nothing.
To undo to step n, walk the log from the end back to step n + 1. Put each before text back. If the before was nothing, remove the file. Last first, or two edits to one file undo in the wrong order.
Worked example: four steps
| Step | Action | Before text |
|---|---|---|
| 1 | Write app.py | x = 1 |
| 2 | Write new.py | nothing: it did not exist |
| 3 | Delete notes.txt | todo |
| 4 | Write app.py | x = 2 |
The cells students fill in are left blank in this preview.
A common mistake
Warning
Many people think git alone can undo an agent. Git only goes back to a commit. It cannot rewind to step six of a run with no commit in it, and it ignores files that are not tracked. Keep your own log of every write.
Write the change log, part 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const files = { \"app.py\": \"x = 1\", \"notes.txt\": \"todo\" };' 'const ops = [' ' { op: \"write\", path: \"app.py\", text: \"x = 2\" },' ' { op: \"write\", path: \"new.py\", text: \"y = 1\" },' ' { op: \"delete\", path: \"notes.txt\" },' ' { op: \"write\", path: \"app.py\", text: \"x = 3\" },' '];' 'function copy(f) { return Object.assign({}, f); }' > undo5.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the change log, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Before every op, log what the file held: its text, or null when it did not exist.' 'function applyRun(start, list) {' ' const now = copy(start);' ' const log = [];' ' list.forEach(function (o, i) {' ' log.push({ step: i + 1, path: o.path, before: o.path in now ? now[o.path] : null });' ' if (o.op === \"write\") now[o.path] = o.text;' ' else delete now[o.path];' ' });' ' return { files: now, log: log };' '}' >> undo5.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the change log, part 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// Undo the entries after a step, last first.' 'function undoTo(now, log, step) {' ' const back = copy(now);' ' for (const e of log.slice().reverse()) {' ' if (e.step <= step) continue;' ' if (e.before === null) delete back[e.path];' ' else back[e.path] = e.before;' ' }' ' return back;' '}' >> undo5.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the change log, part 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function show(label, f) { console.log(label + \" \" + JSON.stringify(f)); }' 'const run = applyRun(files, ops);' 'show(\"start\", files);' 'show(\"after 4 steps\", run.files);' 'show(\"undo to 3\", undoTo(run.files, run.log, 3));' 'show(\"undo to 1\", undoTo(run.files, run.log, 1));' 'show(\"undo to 0\", undoTo(run.files, run.log, 0));' 'console.log(\"log: \" + run.log.map(function (e) { return e.step + \":\" + e.path; }).join(\" \"));' >> undo5.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Run and rewind
docker exec -w /home/node/project agent node undo5.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Files are a dictionary here
The files live in a dictionary in the script, so nothing on disk changes. A real harness writes real files, and keeps the log in a file or a store of its own.
The Run calls no model, so it needs no helper and has nothing to fall back on.
Output of the Run
| Line | What it shows |
|---|---|
| start {"app.py":"x = 1","notes.txt":"todo"} | The files before the run |
| after 4 steps {"app.py":"x = 3","new.py":"y = 1"} | The run wrote, added and deleted |
| undo to 3 {"app.py":"x = 2","new.py":"y = 1"} | Only step 4 was undone |
| undo to 1 {"app.py":"x = 2","notes.txt":"todo"} | Steps 2 to 4 were undone |
| undo to 0 ... | The files as they started |
| log: 1:app.py 2:new.py 3:notes.txt 4:app.py | One entry per step |
The cells students fill in are left blank in this preview.