Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

How to check what an AI agent changed

After this lesson: I can check what a run changed instead of what it said.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

It said it refunded the order

An end-state check compares what a run changed with what it should have changed. It matters because an agent writes a confident report whether or not the work was done.

Two runs of the shop's agent say "Done, A17 is refunded". Only one of them did it.

By the end of this lesson

Note

I can check what a run changed instead of what it said.

Key words

WordWhat it means
End statewhat the world holds when the run stops
Expected statethe end state a correct run leaves
Differencea place where the two states disagree
Side effecta change the task never asked for
Recordone item in the state, such as an order

The cells students fill in are left blank in this preview.

Compare the state, not the sentence

Most agents change something: a file, a row, a ticket. So check the end state. Build a fresh copy of the world, replay the run on it and compare the result with the expected state.

Every place where the two disagree is a difference. An empty list of differences is a pass.

Worked example: the three recorded runs

RunWhat it saidWhat it didState check
AA17 is refundedrefunded A17matches
BA17 is refundedwrote a note onlyA17 is still paid
CRefunded A17refunded A17 and B02B02 changed, and it should not have

The cells students fill in are left blank in this preview.

Look for changes nobody asked for

A run can do the task and still do harm. Run C refunded A17 as asked, then refunded a second order too. The check only finds it if it looks at every record.

A side effect is a record that changed although the task wanted it left alone. Compare the state before with the state after, and list those records.

Write the model helper

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '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 || 200 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    return (await res.json()).choices[0].message.content;' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    const list = [].concat(sample);' '    const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' '    return list[Math.min(turn, list.length - 1)];' '  }' '}' '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 shop and the runs

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const start = {' '  A17: { status: \"paid\", amount: 1200 },' '  B02: { status: \"shipped\", amount: 800 },' '};' 'const expected = {' '  A17: { status: \"refunded\", amount: 1200 },' '  B02: { status: \"shipped\", amount: 800 },' '};' '// Recorded runs. Each says one thing and does another list of things.' 'const runs = [' '  { name: \"Run A\", said: \"Done, A17 is refunded.\", actions: [[\"refund\", \"A17\"]] },' '  { name: \"Run B\", said: \"Done, A17 is refunded.\", actions: [[\"note\", \"A17\"]] },' '  { name: \"Run C\", said: \"Refunded A17.\", actions: [[\"refund\", \"A17\"], [\"refund\", \"B02\"]] },' '];' 'module.exports = { start, expected, runs };' > shop.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the state check, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { start, expected, runs } = require(\"./shop\");' 'const { ask } = require(\"./model\");' 'function copy(state) { return JSON.parse(JSON.stringify(state)); }' 'function apply(actions) {' '  const state = copy(start);' '  actions.forEach(function (action) {' '    if (action[0] === \"refund\") state[action[1]].status = \"refunded\";' '  });' '  return state;' '}' > check.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the state check, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function differences(want, got) {' '  const lines = [];' '  Object.keys(want).sort().forEach(function (id) {' '    Object.keys(want[id]).sort().forEach(function (field) {' '      if ((want[id][field] === got[id][field]) === false) {' '        lines.push(id + \".\" + field + \": expected \" + want[id][field] + \", got \" + got[id][field]);' '      }' '    });' '  });' '  return lines;' '}' >> check.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the state check, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'runs.forEach(function (run) {' '  const found = differences(expected, apply(run.actions));' '  console.log(run.name + \" said: \" + run.said);' '  console.log(run.name + \" state: \" + (found.length === 0 ? \"matches\" : found.join(\"; \")));' '});' 'async function main() {' '  const prompt = \"The refund tool returned an error for order A17. Write one short sentence reporting this to your manager.\";' '  const sample = \"The refund for order A17 failed with an error, so the order is still paid.\";' '  const text = await ask([{ role: \"user\", content: prompt }], sample, 60);' '  console.log(\"Live report: \" + text.trim());' '  console.log(\"State check: A17 is still \" + start.A17.status + \", whatever the words say.\");' '}' 'main();' >> check.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Check the three end states

Command
docker exec -w /home/node/project agent node check.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

What the check prints

LineWhat it shows
Run Amatches
Run BA17.status: expected refunded, got paid
Run CB02.status: expected shipped, got refunded
Live reportOne possible output: a sentence about the failed refund
State checkA17 is still paid

The cells students fill in are left blank in this preview.

Try it

Tip

In shop.js give Run A the actions refund A17 and note A17. Check again. The state still matches, because a note changes nothing the expected state holds.

A common mistake

Warning

Many people compare only the fields the task names. That passes a run that also deleted something else. Compare the whole state, or list the fields you decided to ignore and why, such as a timestamp that changes on every run.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.