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 AI agent in Node

After this lesson: I can build a small agent that plans, calls tools, remembers and stops.

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

Putting the parts together

A small AI agent is one loop that plans, calls tools, keeps notes and stops on a rule. You have built each part on its own. The skill is to wire them so the loop is easy to test.

A loop that only works with a live model cannot be tested. A loop over a scripted model can.

By the end of this lesson

Note

I can build a small agent that plans, calls tools, remembers and stops.

Key words

WordWhat it means
Agent looppick an action, run it, record it, repeat
Scripted modela stand-in that returns fixed actions
Tracea list of what each step did
Finish actionthe action that ends a run as done
Test doublea stand-in used instead of the real part

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

The agent loop in one picture

Every pass checks the budget first. The model picks an action. A finish action ends the run as done. Any other action runs a tool and records the result.

Then the loop counts failures in a row. Enough of them end the run as stuck. Otherwise it goes round again.

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\";' '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;' > 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' '  } 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 small agent, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' 'const PRICES = { pear: 45, bread: 120 };' 'const notes = [];' 'const tools = {' '  get_price: function (item) {' '    if (PRICES[item] === undefined) return { ok: false, text: \"no price for \" + item };' '    return { ok: true, text: item + \" \" + PRICES[item] + \"p\" };' '  },' '  add_note: function (text) { notes.push(text); return { ok: true, text: \"noted \" + notes.length }; },' '};' > agent1.js"

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

Write the small agent, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const script = [' '  { tool: \"get_price\", arg: \"pear\" },' '  { tool: \"get_price\", arg: \"kiwi\" },' '  { tool: \"get_price\", arg: \"bread\" },' '  { tool: \"add_note\", arg: \"pear 45p, bread 120p\" },' '  { tool: \"finish\", arg: \"A pear and bread cost 165p.\" },' '];' '// A scripted model gives the same action every run, so the loop can be tested.' 'const scriptedModel = function (turn) { return JSON.stringify(script[turn]); };' >> agent1.js"

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

Write the small agent, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  const budget = Number(process.argv[2] || 6), maxFails = 2;' '  let used = 0, fails = 0, outcome = \"budget\", answer = \"\";' '  while (used < budget && used < script.length) {' '    const action = JSON.parse(scriptedModel(used));' '    used += 1;' '    if (action.tool === \"finish\") { outcome = \"done\"; answer = action.arg; break; }' '    const run = tools[action.tool];' '    const r = run ? run(action.arg) : { ok: false, text: \"unknown tool \" + action.tool };' '    fails = r.ok ? 0 : fails + 1;' '    console.log(\"Step \" + used + \": \" + action.tool + \"(\" + action.arg + \") -> \" + r.text);' '    if (fails === maxFails) { outcome = \"stuck\"; break; }' '  }' '  console.log(\"Outcome: \" + outcome + \" after \" + used + \" steps. Notes: \" + JSON.stringify(notes));' >> agent1.js"

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

Write the small agent, part 4

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' '  if (outcome === \"done\") {' '    const ask1 = [{ role: \"user\", content: \"Say this to a shopper in one friendly sentence: \" + answer }];' '    console.log(\"Reply: \" + await ask(ask1, answer, 60));' '  }' '}' 'main();' >> agent1.js"

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

Run the agent with a budget of 6

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

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

Run it again with a budget of 4

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

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

One possible output

BudgetWhat it shows
6Step 2 fails for the kiwi, then Outcome: done after 5 steps, and a one-sentence reply
4Four steps, then Outcome: budget after 4 steps, and no reply

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

Test the loop on a scripted model

A scripted model returns the same actions every run. With it you can test the loop for each outcome: done, budget, stuck and ended.

Test the real model separately, on what it does best: choosing a sensible action. Never judge the loop by one lucky live run.

A common mistake

Warning

Many people think an agent that worked once on a real model is tested. A model gives different actions each time, so one good run proves very little. Test the loop on a script, then test the model on many cases.

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.