A Mini Agent in Python
Put it together: a TOOLS dict, JSON parse, a budgeted loop, a transcript, a finish tool, a fake model, and a printed trace.
This lesson puts the pieces together. You will run a mini agent in the browser. There is no paid model. A fake model is a function that returns the next action as JSON.
This is the shape of every later agent lesson. Other libraries wrap this. These objects stay: tools, parse, loop, transcript, finish, budget, trace. If you cannot find them in a framework, they are still there under a new name.
You already have the skills: dicts, functions, json, while/for, exceptions, unpacking **args. The new work is wiring, not new syntax.
The pieces
| Piece | Job |
|---|---|
TOOLS | Dict: name → function |
parse_action | JSON text → {tool, args} |
| Loop | Repeat until finish or budget |
max_steps | The budget — a hard cap |
transcript | A list of what happened |
finish | A tool that means “stop, here is the answer” |
| Fake model | Returns the next JSON action |
The loop is: ask the model, parse, call the tool, append to the transcript, print the trace, repeat.
Tools, parse, transcript, finish, budget. When the budget hits, stop even if the goal is not done.
The mini agent loopEach piece can be tested alone. Together they are an agent. Missing stop is an infinite furnace. Missing parse is a crash. Missing tools dict is eval-by-accident if you were tempted. We will not be tempted.
A fake model
A real model is an HTTP call. Here the “model” is a function. It reads nothing fancy. It returns the next string from a script. You still parse that string. You still dispatch tools. The loop is real. The model is a list of strings.
def make_model(lines):
i = {class="tok-s">"n": 0}
def fake_model(_transcript):
if i[class="tok-s">"n"] >= len(lines):
return class="tok-s">'{"tool": "finish", "args": {"text": "stop"}}'
text = lines[i[class="tok-s">"n"]]
i[class="tok-s">"n"] += 1
return text
return fake_modeli is a dict so the inner function can update n without a global. A list with one integer would also work. The fake ignores the transcript on purpose so the script is predictable. A real model would see the transcript. Tests want predictable.
If the script runs out, we still return a finish JSON. That is a seatbelt. The budget is the other seatbelt.
The loop
Each step costs 1 budget. If parse fails, append an error observation and continue (or stop — your choice). If the tool is finish, stop. If max_steps is used up, stop even if the goal is unmet. Models do not reliably halt. Your Python must.
Print every step. That printout is a trace. If you cannot see what happened, you cannot debug it. Append to the transcript in one place. If every tool logs differently, you will never test a full run.
The else attached to the for runs only if the loop never breaks. If finish never fires, you print stop: budget. That is the same loop-else idea from Loops and Budgets.
TOOLS[tool](**args) is the dispatch. Wrap it in try/except so a bad argument becomes an observation, not a dead process. Unknown tools are rejected in parse if you check tool not in TOOLS. Do both if you like belts and braces. One check is enough if it is always on.
Try it: a tiny agent
TOOLS maps "search" and "finish" to functions. *args unpacks the dict into named inputs. Reuse the idea* of parse_action from the JSON lesson. The fake model emits clean JSON, so the parser can stay short. Production should swap in the fence-stripping parser.
What you still add later
- The robust fence-stripping parser
- Retries on 429 and timeout
goal_satisfiedas a stop check- Tests for parse, budget, and tools
- A real model over HTTP on your laptop
The shape does not change.
Wire one transcript per run_agent call. The function already does transcript = [{"role": "user", "content": goal}]. Two calls in the tryit are two lists. If you accidentally make transcript a global, run 2 would start with run 1’s rows. That is the mutability lesson at agent scale.
Print step before you call the tool. Then a hang is still diagnosable: you know which step started. After the tool, print the result. The tryit does both. When you add retries, print attempt numbers inside the tool, not as extra agent steps, unless you want them to cost budget.
Common mistakes
- No budget.
- Crashing on bad JSON.
- Sharing one transcript across runs.
- Letting the model invent tool names that are not in
TOOLS. - Printing without appending, or appending without printing, so debug and state diverge.
Run to execute this in your browser. Nothing is sent to a server.
Run 1 should search, then finish, and print the transcript. Run 2 should stop after one step: budget. Raise a bad JSON line in the script and watch the error observation. That is the whole agent: tools, parse, loop, trace, cap.
How agents use this
You now have the Python core of a tool-using agent: a TOOLS dict, JSON actions, a fake (then later real) model, a transcript, a finish tool, and a budget. Later tracks plug retrieval, planning, and production into this loop. When a library feels magical, find these objects. They are always there. This is the shape of every later agent lesson.
Swap the fake model for an HTTP POST when you have a laptop, a venv, and a key in the environment. Do not print the key. Keep parse_action. Keep TOOLS. Keep max_steps. Keep printing the trace. The model vendor is a detail. The loop is the product.
If finish never comes, the for-else prints budget and you still have a transcript. That transcript is how you write the next test: copy a failing raw line into parse_action tests. The mini agent is not the end of Python. It is the first program that behaves like the rest of this academy.
Check your understanding