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

Free lesson

Shared state between agents

After this lesson: I can let agents share a record without losing each other's writes.

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

Two agents, one record, one lost update

Shared state is a record that several agents read and write. It matters because two agents that both read, then both write, can silently erase one another's work.

Nothing crashes. The record simply holds the last write, and a good finding is gone.

By the end of this lesson

Note

I can let agents share a record without losing each other's writes.

Key words

WordWhat it means
Shared recordone copy of the facts that every agent reads and writes
Messagetext sent from one agent to another
Versiona number that rises with every accepted write
Base versionthe version a writer read before it wrote
Conflicta write based on a field that has since changed
Lost updatea write that erases another one unseen

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

Messages or a shared record

Agents can pass messages. Each keeps its own copy of what it has heard, and nothing conflicts, but the copies drift apart.

Or they can share one record. There is one truth, but now two writers can collide. Most teams need both: messages to say what to do, a record for what is known.

Worked example: messages against a record

MessagesShared record
Who holds the factsEach agent, separatelyOne place
Can two agents disagreeYes, and nobody knowsNo, there is one value
Can a write be lostNoYes, unless writes are checked
Best forTelling a worker what to doWhat the team has found so far

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

A write names the version it read

Give every field a version. When an agent reads, it keeps the version. When it writes, it sends that base version back.

The record accepts the write only if the field has not changed since. Otherwise it answers with a conflict, and the agent reads again and decides. A field nobody has written yet cannot conflict.

A common mistake

Warning

Many people settle a conflict by letting the later write win. That is the lost update again, with a log line. A conflict is a question for the writer: read the new value, then decide.

Write the model helper, part 1

Command
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

Command
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 shared record, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const record = { version: 0, fields: {} };' 'function read() {' '  return { version: record.version, fields: JSON.parse(JSON.stringify(record.fields)) };' '}' > state.js"

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

Write the shared record, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// A write names the version it read. It applies only if nobody changed that field since.' 'function write(agent, field, value, base) {' '  const f = record.fields[field];' '  const word = \" writes \" + field + \" from version \" + base + \": \";' '  if (f && f.version > base) {' '    console.log(agent + word + \"CONFLICT, held at version \" + f.version + \" by \" + f.by);' '    return false;' '  }' '  record.version += 1;' '  record.fields[field] = { value: value, version: record.version, by: agent };' '  console.log(agent + word + \"applied, record at version \" + record.version);' '  return true;' '}' >> state.js"

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

Write the shared record, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const a = read();' '  const b = read();' '  write(\"pricer\", \"cost\", \"108 pounds\", a.version);' '  if (write(\"reader\", \"cost\", \"120 pounds\", b.version) === false) {' '    const again = read();' '    console.log(\"reader reads again: cost is \" + again.fields.cost.value + \" at version \" + again.version);' '    write(\"reader\", \"cost_note\", \"second figure was 120\", again.version);' '  }' '  write(\"reader\", \"wants\", \"212 of 400\", b.version);' '  await last();' '}' >> state.js"

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

Write the shared record, part 4

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function last() {' '  const q = [{ role: \"user\", content: \"Write one note, under 12 words, saying two figures for one cost disagree.\" }];' '  const note = (await ask(q, \"Two figures for one cost disagree, so check the source.\", 30)).text.trim();' '  write(\"reviewer\", \"note\", note, record.version);' '  const names = Object.keys(record.fields);' '  console.log(\"Fields: \" + names.map(function (k) { return k + \"@\" + record.fields[k].version; }).join(\", \"));' '}' 'main();' >> state.js"

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

Run the shared record

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

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

This is a stand-in

The lab has no database, so the record is an object in the script. A real team uses a database that does this check inside one step. Checking and then writing as two steps leaves a gap for another writer.

The one live call words a note. Its words differ each run.

One possible output

LineWhat it shows
pricer writes cost from version 0: applied, record at version 1The first writer wins
reader writes cost from version 0: CONFLICT, held at version 1 by pricerThe second had read an old version
reader writes cost_note from version 1: applied, record at version 2After reading again, it writes a note
reader writes wants from version 0: applied, record at version 3A new field cannot conflict
Fields: cost@1, cost_note@2, wants@3, note@4The version each field was last written at

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

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.