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
| Word | What it means |
|---|---|
| Shared record | one copy of the facts that every agent reads and writes |
| Message | text sent from one agent to another |
| Version | a number that rises with every accepted write |
| Base version | the version a writer read before it wrote |
| Conflict | a write based on a field that has since changed |
| Lost update | a 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
| Messages | Shared record | |
|---|---|---|
| Who holds the facts | Each agent, separately | One place |
| Can two agents disagree | Yes, and nobody knows | No, there is one value |
| Can a write be lost | No | Yes, unless writes are checked |
| Best for | Telling a worker what to do | What 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
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
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
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
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
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
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
docker exec -w /home/node/project agent node state.jsShown 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
| Line | What it shows |
|---|---|
| pricer writes cost from version 0: applied, record at version 1 | The first writer wins |
| reader writes cost from version 0: CONFLICT, held at version 1 by pricer | The second had read an old version |
| reader writes cost_note from version 1: applied, record at version 2 | After reading again, it writes a note |
| reader writes wants from version 0: applied, record at version 3 | A new field cannot conflict |
| Fields: cost@1, cost_note@2, wants@3, note@4 | The version each field was last written at |
The cells students fill in are left blank in this preview.