フォルダを元のとおりに戻す
スナップショットは、エージェントが動く前に取る、フォルダのすべてのファイルのコピーです。巻き戻しは、フォルダをそのコピーへ戻すことです。
閉じ込めたエージェントでも、触ってよいファイルを台なしにすることはあります。スナップショットがあれば、「すべて、そっくり元に戻して」と言えます。
このレッスンのゴール
メモ
エージェントが動く前のとおりに、フォルダを戻すことができる。
キーワード
| ことば | いみ |
|---|---|
| スナップショット | すべてのファイルと、その文章のコピー |
| 差分 | 二つの状態の間で何が変わったかの一覧 |
| 復元 | スナップショットからファイルを戻すこと |
| 巻き戻し | フォルダ全体の復元 |
| 増えたファイル | スナップショットになかったファイル |
学生が入力するセルは、このプレビューでは空白です。
スナップショットはすべてを持つ
エージェントが始まる前に、フォルダのファイルをすべて読み、パスと文章を残します。残す場所は、エージェントが届かないところです。
パスで比べます。エージェントが動いたあと、新しければ増えた、なくなっていれば消えた、文章が違えば変わったです。
例: 三つの編集
| エージェントの編集 | 差分では | 戻すには |
|---|---|---|
| src/app.js を新しい文章で書いた | 変わった | 古い文章を書く |
| notes.txt を消した | 消えた | ファイルを書き直す |
| new.txt を作った | 増えた | それを消す |
| ファイルを古い文章に書き戻した | 何もない | することはない |
学生が入力するセルは、このプレビューでは空白です。
よくある間違い
注意
復元は、変わったファイルを書き直せばよい、と考える人が多いです。増えたファイルも消さなければ、フォルダは元のとおりになりません。そして確かめます。復元のあとで、差分は空でなければいけません。
復元できたことを示す
確かめない復元は、願いにすぎません。復元したあと、新しいスナップショットを取り、最初のものと比べます。差分が空なら、フォルダは元のとおりです。
この比べ方はファイルだけを見ます。エージェントが作った空のフォルダは、ファイルの一覧に出ません。スナップショットが覆わないものを、言葉にしておきましょう。
助っ人 model.js を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// 応答のトークン数。なければ null。' 'function count(data, name) {' ' return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
助っ人 model.js を書く(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(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' return { text: sample, inTokens: null, outTokens: null, recorded: true };' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
巻き戻しのスクリプトを書く(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"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
巻き戻しのスクリプトを書く(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"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
巻き戻しのスクリプトを書く(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(\"スナップショット: \" + Object.keys(snap).join(\" \"));' ' put(\"src/app.js\", \"let x = 2;\");' ' fs.rmSync(path.join(root, \"notes.txt\"));' ' put(\"new.txt\", \"hello\");' ' console.log(\"変更: \" + JSON.stringify(diff(snap, take())));' ' restore(snap);' ' console.log(\"復元後も同じ: \" + (JSON.stringify(take()) === JSON.stringify(snap)));' ' const q = \"短く一文で答えてください。エージェントがファイルを編集する前に、スナップショットを取るのはなぜですか。\";' ' const sample = \"編集がうまくいかなかったとき、すべてのファイルを元のとおりに戻せるからです。\";' ' const reply = await ask([{ role: \"user\", content: q }], sample, 80);' ' console.log(\"モデル: \" + reply.text);' '}' 'main();' >> rollback.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
巻き戻しを動かす
docker exec -w /home/node/project agent node rollback.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
Run の出力
| 行 | 意味 |
|---|---|
| スナップショット: notes.txt src/app.js src/util.js | snap-lab にある三つの本物のファイル |
| 変更: {"added":["new.txt"],"changed":["src/app.js"],"removed":["notes.txt"]} | 身代わりのエージェントがしたこと |
| 復元後も同じ: true | フォルダがまたスナップショットと等しい |
| モデル: ... | 一文。言葉は毎回変わる |
学生が入力するセルは、このプレビューでは空白です。