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

Free lesson

Build a small coding agent harness

After this lesson: I can put context, tools, modes, isolation and undo into one harness that I can run and rewind.

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

Put the pieces in one loop

An agent harness is the program around the model: it builds the window, runs the tools, applies the mode and records the changes. The model only proposes a step. The harness decides what happens.

This lesson joins the five earlier ones into one loop you can run and rewind.

By the end of this lesson

Note

I can put context, tools, modes, isolation and undo into one harness that I can run and rewind.

Key words

WordWhat it means
Harnessthe program that runs an agent around a model
Loopask for a step, decide, act, record, repeat
Scripted modela fixed list of steps that stands in for a model
Transcriptthe record of each step and its outcome
Windowthe recent results sent to the model next
Rewindundoing every change after a chosen step

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

The loop, step by step

Each turn the harness asks the model for one step. It decides by mode: allow, ask or deny. If it must ask, the user answers. If the step is allowed, the harness logs what it will replace, then does it.

The result goes into a window, which keeps only the latest few. Then the loop repeats until the model says it is done.

What each earlier lesson adds

LessonWhere it sits in the loop
ContextThe window keeps only the latest results
ToolsEach step names a narrow tool
ModesOne function decides allow, ask or deny
IsolationA side task returns a short result into the window
UndoA log entry is written before a change

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

A common mistake

Warning

Many people let a refused step vanish. The model then never learns it was refused and asks again. Record the refusal as a result, so the next window shows it.

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 harness, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const RISK = { read_file: \"read\", write_file: \"write\", delete_file: \"destructive\", run: \"run\" };' 'const script = [' '  { tool: \"read_file\", path: \"app.py\" },' '  { tool: \"write_file\", path: \"app.py\", text: \"x = 2\" },' '  { tool: \"run\", path: \"pytest\" },' '  { tool: \"delete_file\", path: \"notes.txt\" },' '  { tool: \"write_file\", path: \".env\", text: \"KEY=1\" },' '  { tool: \"done\" },' '];' 'const files = { \"app.py\": \"x = 1\", \"notes.txt\": \"todo\" };' 'const log = [];' > harness6.js"

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

Write the harness, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function decide(mode, risk, path) {' '  if (mode === \"plan\") return risk === \"read\" ? \"allow\" : \"deny\";' '  if (path.startsWith(\".env\") || risk === \"destructive\") return \"ask\";' '  if (mode === \"auto\" && (risk === \"read\" || risk === \"write\")) return \"allow\";' '  if (mode === \"ask\" && risk === \"read\") return \"allow\";' '  return \"ask\";' '}' >> harness6.js"

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

Write the harness, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function act(s, n) {' '  if (s.tool === \"read_file\") return s.path in files ? files[s.path] : \"missing\";' '  if (s.tool === \"run\") return \"ran \" + s.path;' '  log.push({ step: n, path: s.path, before: s.path in files ? files[s.path] : null });' '  if (s.tool === \"write_file\") { files[s.path] = s.text; return \"wrote \" + s.path; }' '  delete files[s.path];' '  return \"deleted \" + s.path;' '}' >> harness6.js"

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

Write the harness, part 4

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function runAll(mode, approvals) {' '  const out = [];' '  for (let i = 0; i < script.length; i++) {' '    const s = script[i];' '    if (s.tool === \"done\") break;' '    const v = decide(mode, RISK[s.tool], s.path);' '    let outcome = v === \"allow\" ? \"ok\" : \"denied\";' '    if (v === \"ask\") outcome = approvals.shift() === true ? \"approved\" : \"refused\";' '    const good = outcome === \"ok\" || outcome === \"approved\";' '    out.push({ step: i + 1, tool: s.tool, path: s.path, outcome: outcome, result: good ? act(s, i + 1) : outcome });' '  }' '  return out;' '}' >> harness6.js"

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

Write the harness, part 5

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function undoTo(step) {' '  for (const e of log.slice().reverse()) {' '    if (e.step <= step) continue;' '    if (e.before === null) delete files[e.path];' '    else files[e.path] = e.before;' '  }' '}' >> harness6.js"

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

Write the harness, part 6

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const out = runAll(\"auto\", [true, false]);' '  for (const o of out) console.log(o.step + \" \" + o.tool + \" \" + o.path + \" \" + o.outcome + \" -> \" + o.result);' '  console.log(\"window \" + JSON.stringify(out.slice(-3).map(function (o) { return o.result; })));' '  console.log(\"files \" + JSON.stringify(files));' '  undoTo(1);' '  console.log(\"after undo to 1 \" + JSON.stringify(files));' '  const text = out.map(function (o) { return o.tool + \" \" + o.outcome; }).join(\", \");' '  const reply = await ask([{ role: \"user\", content: \"Write a one line commit message for a run that did this: \" + text }], \"Fix the basket total in app.py\", 30);' '  console.log(\"message: \" + reply.text);' '}' 'main();' >> harness6.js"

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

Run the harness and rewind

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

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

The model is a script

The steps come from a fixed list, so every student prints the same run. A real harness would ask the model for each step. The one short live call only writes a commit message from what the run did.

With no model it prints a Recorded sample for that line.

One possible output

LineWhat it shows
1 read_file app.py ok -> x = 1A read passes in auto mode
3 run pytest approved -> ran pytestThe first approval was yes
4 delete_file notes.txt refused -> refusedThe second approval was no
5 write_file .env refused -> refusedNo approval was left, so no
files {"app.py":"x = 2","notes.txt":"todo"}Only the write changed anything
after undo to 1 {"app.py":"x = 1",...}The rewind put the write back

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.