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
| Word | What it means |
|---|---|
| Agent loop | pick an action, run it, record it, repeat |
| Scripted model | a stand-in that returns fixed actions |
| Trace | a list of what each step did |
| Finish action | the action that ends a run as done |
| Test double | a 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
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
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
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
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
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
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
docker exec -w /home/node/project agent node agent1.jsShown 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
docker exec -w /home/node/project agent node agent1.js 4Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
One possible output
| Budget | What it shows |
|---|---|
| 6 | Step 2 fails for the kiwi, then Outcome: done after 5 steps, and a one-sentence reply |
| 4 | Four 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.