Systems & Software
minicode
A coding agent in the terminal, handwritten to understand the harness
- Role
- Development
- Context
- Personal project
- Stack
- Python, Anthropic SDK, Ollama, rich, prompt_toolkit
Overview
A mini Claude Code in Python, with no agent framework. All the code around the model is handwritten: the agent loop, 8 tools including interactive sessions in a pseudo-terminal, allow / deny permissions, handling of an 8,192-token window, read-only sub-agents, a JSONL log of every call and 13 eval tasks. It runs locally and for free, with Ollama and qwen3:8b.
11/13
eval tasks passed locally by qwen3:8b
Highlights
- 01Agent loop, tools and permissions with no framework
- 02Context managed by clearing then summarising, sub-agents with an empty history
- 03Evals from 5/10 to 11/13 with qwen3:8b, by making the harness generic
Architecture
Under the hood
A coding agent in the terminal, in the style of Claude Code, handwritten in Python with no agent framework. The model only asks for tools; everything else is the harness: the loop that calls it again, the tools that act on the project, the permissions that decide, the handling of an 8,192-token window, sub-agents and a log of every call. It runs for free locally, with Ollama and qwen3:8b.
- lines of Python, excluding tests and evals
- 2 343lines of Python, excluding tests and evals
- tools, including task for sub-agents
- 8tools, including task for sub-agents
- tokens of context window by default, locally
- 8 192tokens of context window by default, locally
- tests, with no model call
- 75tests, with no model call
- eval tasks passed by qwen3:8b
- 11/13eval tasks passed by qwen3:8b
- roadmap milestones, all shipped
- 10roadmap milestones, all shipped
Who talks to whom
Terminal
ui.py
rich and prompt_toolkit: input, live thinking, diff, permission choice
one request, appended to messages
Harness
minicode.py
_agent_loop: calls the model again while it asks for tools, 30 calls at most
context.py
Estimates tokens, clears old results, summarises past 70%
agents_md.py
AGENTS.md from the project and parent folders, in the system prompt
tool_use returned by the model
Guards
tools.precheck
A call bound to fail goes back to the model without asking you
permissions.py
deny, then compound command, then allow, otherwise ask
run_tool(name, input)
Tools
tools.py
read_file, list_dir, grep, edit_file, bash, interactive_start, interactive_send
subagent.py
task: a second read-only loop, 12 calls at most
subprocess, pseudo-terminal
Project
Workspace
Paths relative to the project, bash stopped after 60 s
.minicode/
Permission rules, history, JSONL logs
External services
Ollama · qwen3:8b
Local and free, on the GPU
Design principles
01
The model has no memory
The conversation is a plain list, messages, sent in full on every call. /clear empties it, a sub-agent gets a fresh one.
02
The model runs nothing
It answers tool_use; the harness runs it and sends back a tool_result with the same id. Even a refusal sends a tool_result, or the API rejects the history.
03
The harness decides
Before edit_file, bash or interactive_start: a deny rule wins even in YOLO mode, and a compound command (; && | $( …) is always asked.
04
Small, checked edits
edit_file replaces an exact text under 15 lines, says where the text diverges, and refuses anything that would break a valid Python or JSON file.
05
Context is managed cheapest first
Clear old tool results, then summarise earlier turns, then keep only the last result. A tool_use is never split from its tool_result.
06
Measure instead of trying
Every call is logged as JSONL, and 13 fixed tasks run in fresh folders. A prompt change is judged on the score, not on one try.
One turn of the loop
- 01fit_contextContextBefore each call: estimate, then clear or summarise past 70%
- 02call_modelCallStreamed; the real usage recalibrates the next estimate
- 03precheckCheckArguments validated, edit_file simulated before any question
- 04ask_permissionPermissionDeny rule, allow rule, YOLO, or a question to the user
- 05run_toolExecutionOutput capped at ~20% of the window, exit code appended
↳ The model is done
- No more tool requested: its text is the answer
- Empty answer: the harness nudges it once
↳ The harness stops
- The same failed call 3 times: hint on the 2nd, stop on the 3rd
- 30 calls with no final answer
- refusal, max_tokens or Ctrl-C: history restored to its state before the request
A sub-agent follows the same loop, with its own history and its own budget. Only its final report becomes the tool_result of task.
Example · “run the tests; if one fails, fix it”
- 01UI → Harnessrequest
- 02Harness → LLMstream(messages)
- 03LLM → Harnesstool_use bash
- 04Harness → Toolstests: exit code 1
- 05LLM → Harnesstool_use grep, then read_file
- 06LLM → Harnesstool_use edit_file
- 07Harness → UIdiff and permission choice
- 08UI → Harness1. Yes
- 09Harness → Toolsedit_file calc.py
- 10LLM → Harnesstool_use bash
- 11Harness → Toolstests: exit code 0
- 12LLM → UIend_turn: answer
A real run with qwen3:8b on a calc.py where add subtracts: the model chains bash, grep, read_file, edit_file and bash, unaided. Between two calls, each tool_result goes back to the model with the whole history. Here bash is allowed by an allow rule; edit_file is asked.
Evals: two runs of qwen3:8b
| Task | 1st run | 2nd run | Finding |
|---|---|---|---|
| lire_valeur | ✗ | ✓ | Not targeted by the changes: passed by chance |
| corriger_bug | ✗ | ✓ | Finally runs python3 test_calc.py instead of pytest |
| renommer | ✗ | ✓ | Checks by running main.py |
| gros_fichier | ✗ | ✓ | Reads the rest with start_line |
| compteur_jeu | ✗ | ✗ | Finds the bug, describes the fix, does not apply it |
| corriger_js | — | ✓ | Finds npm test on its own in package.json |
| corriger_shell | — | ✗ | “Fix line 4” instead of doing it |
| modifier_json | — | ✓ | — |
| 5 other tasks | ✓ | ✓ | — |
5/10 then 11/13, one try per task: qwen3 is random. Both remaining failures share one cause: the model describes the action instead of taking it.
What qwen3:8b taught the harness
| Observed | Harness change |
|---|---|
| Copies the example from a tool description | No concrete examples left in descriptions |
| Ignores the interactive tools and sticks to bash | interactive_start and interactive_send listed before bash |
| Hardcodes the random number to 50 so its test passes | Rule: never change a program just to pass a test |
| Rewrites the whole file from memory and drops a line | old_string capped under 15 lines, syntax checked |
| Repeats the exact same failed call 6 times | Hint on the 2nd identical failure, stop on the 3rd |
| Tries sudo pacman during an eval in YOLO mode | Deny rules in every eval folder, stronger than YOLO |
Full stack
| Tech | Version | Role |
|---|---|---|
| Python | ≥ 3.12 | Language, no agent framework |
| uv | — | Dependencies and launch (uv run) |
| pytest | 9.1.1 | 75 tests, with no model call |
| Tech | Version | Role |
|---|---|---|
| anthropic | 1.10.0 | Client for the Ollama server, streamed |
| rich | 15.0.0 | Display: Markdown, diff, panels |
| prompt_toolkit | 3.0.53 | Input, history, command completion |
| Tech | Version | Role |
|---|---|---|
| Ollama | — | Local server, API compatible with the Anthropic SDK |
| qwen3:8b | — | The model used, ~5 GB, on 8 GB of VRAM |
Read from the code, commit e2e1442, 2 October 2026
Next project MAGIC
MagicCards →