クリーム色の背景、色付き表示、高コントラスト、大きな文字のほうが読みやすいですか?配色と文字を選ぶと、すべてのページに一度で反映されます。

無料レッスン

スナップショットと巻き戻し

このレッスンのあとで: エージェントが動く前のとおりに、フォルダを戻すことができる。

65分のレッスンコマンド6件AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

フォルダを元のとおりに戻す

スナップショットは、エージェントが動く前に取る、フォルダのすべてのファイルのコピーです。巻き戻しは、フォルダをそのコピーへ戻すことです。

閉じ込めたエージェントでも、触ってよいファイルを台なしにすることはあります。スナップショットがあれば、「すべて、そっくり元に戻して」と言えます。

このレッスンのゴール

メモ

エージェントが動く前のとおりに、フォルダを戻すことができる。

キーワード

ことばいみ
スナップショットすべてのファイルと、その文章のコピー
差分二つの状態の間で何が変わったかの一覧
復元スナップショットからファイルを戻すこと
巻き戻しフォルダ全体の復元
増えたファイルスナップショットになかったファイル

学生が入力するセルは、このプレビューでは空白です。

スナップショットはすべてを持つ

エージェントが始まる前に、フォルダのファイルをすべて読み、パスと文章を残します。残す場所は、エージェントが届かないところです。

パスで比べます。エージェントが動いたあと、新しければ増えた、なくなっていれば消えた、文章が違えば変わったです。

例: 三つの編集

エージェントの編集差分では戻すには
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.jssnap-lab にある三つの本物のファイル
変更: {"added":["new.txt"],"changed":["src/app.js"],"removed":["notes.txt"]}身代わりのエージェントがしたこと
復元後も同じ: trueフォルダがまたスナップショットと等しい
モデル: ...一文。言葉は毎回変わる

学生が入力するセルは、このプレビューでは空白です。

このラボとソースについて: AI コーディングエージェントのラボ:opencode でコードを説明・修正し、その結果を確かめる

授業で使いますか?このモジュールを学生に割り当てると、教員用ガイド、自動採点つきの課題、学生ごとのラボを使えます。