← Writing

Writing Its Own Code: From a Teacher Model to Code Built by Search

Published
Reading
15 min
Contents · 17 sections
  1. Why the “write it” step matters
  2. Stage A: a teacher writes the tool, step by step
  3. The result: 13 of 15, and one failure I can’t explain
  4. What the agent does, and what it doesn’t
  5. Stage B: code built by search, with no language model
  6. The pieces
  7. The search: imagine first, run only the survivors
  8. The result: 13 of 13
  9. What the world model is really doing here
  10. Stage C: learning from traces of real code
  11. Step 1: watch thousands of programs run, line by line
  12. Step 2: mine new building blocks from the traces
  13. Step 3: a small model that predicts exact values
  14. Where Stage C stands right now
  15. Honest limits
  16. Why this matters for “more and more”
  17. Key ideas
data = json.load(open(A))
words = [str(p["city"]) for p in data]
res = collections.Counter(words).most_common(1)[0][0]
with open(B, "w") as f: f.write(str(res) + "\n")

That’s a tiny Python program that reads a JSON list of people and writes down the city where most of them live. It isn’t pretty. It stores city names in a variable called words, because the building block it borrowed was made for counting words.

No language model wrote it. A search program put it together from small pieces, guided by a model it had learned by running those pieces thousands of times. It imagined 6,272 steps, kept 481 programs that looked possible, ran 92 of them for real, and found one that worked in 0.9 seconds. A separate check on a fresh input, which the search never saw, said it was right.

This is the third of my RIAI Lab Notes. The second post showed a terminal agent that learns what commands do, and grows new commands by getting Python tools written. It ended with an honest gap: Claude Haiku writes those tools, not the agent. This post is about closing that gap, one step at a time.


Why the “write it” step matters

My vision for this project fits in one sentence: the terminal does everything; it self-develops; it writes its own Python code to develop skills; and I expect it to keep developing, more and more.

Writing code is the engine. But if a big outside model writes every new skill, the agent isn’t really developing. It’s borrowing. So the question here is: how much of “write the code” can move from a teacher into the agent itself? So far there are three stages:

  1. A teacher writes it. Haiku writes each tool, but in small, tested steps.
  2. Search writes it. No language model. Programs are built from building blocks, guided by a learned world model.
  3. A learned model of code guides it. A small network, trained from scratch on traces of real programs, predicts what each line will do. Work in progress.

Stage A: a teacher writes the tool, step by step

The first method had Haiku write a whole tool plus a test in one go, with up to three tries. In run 5 of post 2, all four tools it wrote passed their own tests, and still 0 of 4 new coding tasks passed the outside check. A tool can pass its author’s test and still be wrong for the real job.

So I wrote a second method, make_tool, that works step by step:

  1. Haiku plans 3 to 5 small steps, like “read the input”, “parse the numbers”, “keep the even ones”, “write the output”. For each step it also says what the result should look like.
  2. Each step is written on its own, added to the code so far, and run on a real sample input inside the VM. When the task’s input file already exists, that real file is the sample.
  3. Haiku looks at what actually came out and judges whether this step did what it should. If not, the step is rewritten right away, with the real output as a hint.
  4. When all steps are in, a final test checks the whole tool. If it fails, the line number in the error is used to find which step caused it, and that’s what gets fixed.

Here’s what that looked like for filter_evens, a tool to keep only the even numbers from a file. These are the real outputs the code produced after each step, on the real input file:

Step What it does What the code so far produced
1 read the lines ['15\n', '39\n', '18\n', '47\n', '80\n', '74\n', '13\n', '84\n']
2 turn them into whole numbers [15, 39, 18, 47, 80, 74, 13, 84]
3 keep the even ones [18, 80, 74, 84]
4 write one per line 18, 80, 74, 84

It’s what a careful programmer does: print the intermediate value and look at it before going on.

The result: 13 of 15, and one failure I can’t explain

In the step-by-step run, 13 of 15 exam tasks passed the host check, with 64 Haiku calls in total, costing about 2 US cents. Six coding task types were new in that run. Five were solved, and the same five passed again when the task came back later, this time with no new code and zero calls.

The one that failed both times was csvavg: “Compute the average of the amount column of a CSV file and write it into avg_amount.txt.”

The strange part is that the coding log looks clean. The tool csv_column_average was built in 4 steps, every step was judged right on its first attempt, the sample run gave a sensible average, and the whole-tool test passed. The tool is in the toolbox. Yet the task failed, the agent’s own self-check said it failed, and the host check agreed. The logs show that it failed, not why.

What the agent does, and what it doesn’t

  • Haiku spots that no known command fits (while turning the task into a target), plans the steps, writes each step, judges each step and writes the final test.
  • The agent’s side of the system runs every step and every test for real in the VM, keeps a tool only if its test passes, registers it as a new command, and then treats it like any other command: its effect is unknown until tried. It practises it five times on copies of the right kind of file, then plans with it like sort or grep.

There are now 11 tools in the toolbox: average, median, most_common_word, sum_csv_column, json_names_over_30, count_vowels, longest_line, csv_column_average, most_common_city, title_case and filter_evens.

That’s progress in reliability, not in independence.


Stage B: code built by search, with no language model

Next question: can programs be built with no language model at all? The coder for this is wmcoder, short for world-model coder. Its report screen says it plainly: “programs built by SEARCH, no LLM writes code”.

The pieces

Building blocks. There are 29 of them, and each is one line of Python with known inputs and outputs. For example:

  • text = open(A).read()
  • lines = [l.strip() for l in text.splitlines() if l.strip()]
  • nums = [float(x) for x in lines]
  • res = sum(nums) / len(nums)
  • with open(B, "w") as f: f.write(str(res) + "\n")

Some blocks have a slot, like a CSV column name or a JSON key. Those slots are filled from what the example input actually contains, such as the CSV header.

A task is given only as examples: two input files and the output each one should produce. Like unit tests, with no description in words.

A world model of the blocks, the terminal agent’s idea applied to code. Before any task, the coder plays. It builds random little programs of 1 to 5 blocks, runs them inside the offline VM on random inputs (lists of numbers, words, sentences, CSV files, JSON files), and records what happened after each block.

From that it learns, for each block and each kind of input, two things:

  1. Does it crash? For example, float() on lines of words crashes.
  2. What kind of value comes out? A number, a list of strings, a list of floats, a file.

In the run I’m reporting, it learned from 88,712 block executions.

The search: imagine first, run only the survivors

To solve a task, the coder grows programs block by block in its head, asking its world model each time: will this crash here, and what kind of value will it give? A program is dropped if a crash is predicted, or if that block was never seen working in that situation. Programs that end by writing an output file, with up to 6 blocks, become candidates.

Then reality decides. The surviving candidates, shortest first, are run on the two examples inside the VM, in batches of 80. The first one that reproduces both examples is the answer. Finally the host runs it on a fresh input it never saw and checks the result.

Task "average": two input files, the output each should give 40,885 imagined steps a block added to a program in its head, checked by the world model 4,213 programs survived complete programs not predicted to crash, using blocks seen working 80 run for real shortest first, on the two examples, inside the offline VM 1 program kept passed the host check on a fresh input it never saw: 5 blocks, 0.9 seconds No language model at any step. The world model only rules out what it expects to fail.
The search funnel for one task: imagine a lot, run a little, and let an outside check decide.

The result: 13 of 13

The coder solved 13 of 13 tasks, every one confirmed by the host check on a fresh input: average, median, total, largest, longest line, count words, most common word, count vowels, even numbers, title case, sort lines, csv average and json top city.

Each took 0.6 to 0.9 seconds. Here’s the program it found for “average”, exactly as saved:

text = open(A).read()
lines = [l.strip() for l in text.splitlines() if l.strip()]
nums = [float(x) for x in lines]
res = sum(nums) / len(nums)
with open(B, "w") as f: f.write(str(res) + "\n")

For text tasks like “count words”, the funnel was 24,094 imagined and 2,806 survived. “json top city”, from the top of this post, was the smallest search.

Notice two things about that JSON program. It found the right key, city, by trying the keys it saw in the example file. And it reused a block meant for words to count cities, because to the world model, both are just “a list of strings”.

Also notice the CSV average. Its Stage A cousin, with a Haiku-written tool, failed. Here, a four-block program built by search passed, though the two exams aren’t identical.

What the world model is really doing here

The world model doesn’t know the goal. It never looks at the expected output when it imagines. All it does is rule out programs it expects to crash, or that use a block in a situation where it has never seen that block work. It rules a block out only if, in that situation, it crashed more than 80% of the time. Everything else goes to reality. The examples do the real choosing.

That’s why the imagined count is identical for tasks on the same kind of input. What the world model buys is efficiency: thousands of plausible programs instead of every possible one.

Guiding program search with a learned model isn’t new: DeepCoder (Balog and others, 2017) trained a network to predict, from examples, which functions a program probably uses, to speed up search. Mine is smaller and cruder, and its model comes from the coder’s own experiments.


Stage C: learning from traces of real code

Stage B has two weak points. I wrote the blocks. And the world model only knows “kinds”, not values, so it can’t tell which surviving program will give the right answer. Stage C goes after both.

Step 1: watch thousands of programs run, line by line

traces.py runs programs inside the offline VM under Python’s built-in tracing hook. For every line that runs, it records which variables changed and to what, which line runs next, what a function returns, and any error. Here’s a real excerpt from the trace, shortened a little, of a small function that appends to a list passed in by reference:

cur = push(prev, 13)        # state: {prev: [4] -> [4, 13], cur: new 2}
    items.append(left)      # state: {items: [4] -> [4, 13]}
    return len(items)       # state: no change | returns 2

In ten minutes it collected:

  • 61,611 programs at about 103 programs per second
  • 982,893 traced lines
  • 4,352 error traces, mostly KeyError (890), IndexError (879), ZeroDivisionError (872), ValueError (868) and TypeError (841)
  • the agent’s own 11 tools, run on sample inputs

Almost all of these programs are synthetic. A generator I wrote makes them, and it’s varied on purpose: branches, loops, recursion, classes, comprehensions, aliasing (two names pointing at the same list), and programs that crash deliberately, some caught and some not. Names and numbers are random, so a model trained on them has to read the code rather than memorise it.

Step 2: mine new building blocks from the traces

mine.py looks through the traced lines for simple ones of the form “new value = expression of one existing value”. It renames the variables by the kind of value they hold, so that x = total // 2 and n = count // 2 both become res = res // 2, and counts how often each abstract line appears. Lines that show up often enough become new building blocks.

In the latest saved block list, there are 40 blocks: the 29 I wrote, plus 11 mined from traces. The most common mined line is res = res // 2, seen 19,373 times. Others include res = res * 2, words = [p.upper() for p in words] and res = '-'.join(words).

None of those four lines exists among my 29 blocks. That matters for the test below.

Step 3: a small model that predicts exact values

The most ambitious piece spans codewm.py, wmcoder2.py and stepwm.py. The idea is a neural code world model: a small character-level transformer, trained from scratch on the GPU with nothing downloaded, that reads a line of code and the values going into it, and predicts the exact value that comes out. Not “a number”, but 47.0.

With that, wmcoder2 would simulate surviving programs on the examples in imagination, rank them by how close the predicted output is to the wanted one, and run only the top few.

stepwm.py, the latest version, uses one training format for every kind of line: assignments, list changes, branches (which line runs next), returns and errors. To fight memorisation, every name in the code is randomly renamed each time an example is used (v_7, f_3 and so on). The test uses whole programs never seen in training, numbers far larger than any in training, and unseen name prefixes.

Where Stage C stands right now

This is work in progress, and I’ll label it that way.

At the time of writing (23 October 2026), there are no results for wmcoder2 or the full stepwm evaluation, and the training files were still being written.

What does exist is a training log from the first short runs of stepwm, three model sizes trained for about six minutes each. On held-out programs, the best of them predicted the exact next step correctly about 2% of the time. None of them got 3 steps in a row right even once. That isn’t a result. It’s a starting line, and an honest one.


Honest limits

  • wmcoder only does short, single-file tasks. Read one file, compute something about text or numbers, write one file. Up to 6 blocks, no branches or functions of its own. Any looping happens inside a single block.
  • The blocks are hand-written, and some are big. “Count vowels” is one block, and so is the median formula. Often the search only had to find the glue around a block that did the hard part, and I chose the blocks knowing the tasks.
  • The exams are small. 13 tasks for wmcoder, 15 for the step-by-step run, two examples per task.
  • Mined blocks come from programs my generator wrote. They’re “learned” in the sense that nobody listed them, but they reflect what that generator likes to produce.
  • The neural code world model has no results yet. The only numbers are early training checkpoints, and they’re near zero.
  • Haiku still writes the terminal agent’s tools. wmcoder is a separate program. It hasn’t been plugged into the terminal agent yet.

Why this matters for “more and more”

Here’s what I expect, stated as a hypothesis to test rather than something I’ve shown.

If the agent really develops itself, each new skill and each new trace should make the next program easier to write: tools get traced, traces become blocks and better predictions, and those make the next search smaller and wider. That loop is what “more and more” would mean in practice.

The first link can be checked, and the check is already written into wmcoder2. It has three new tasks: “caps with dashes”, “half the total” and “double the largest”. As far as I can tell from the block list, none of them can be solved with my 29 hand-written blocks. All of them need blocks that so far exist only because they were mined from traces.

So the test ahead is simple to state:

Can blocks learned from traces solve tasks the hand-written blocks can’t, checked by the host on fresh inputs?

And the second test: with the neural model ranking candidates, does the number of programs that need running drop from about 80 to a handful, without losing tasks?

Yes on both would be small but real evidence that experience compounds. No gets published too. And those three tasks were chosen with the mined blocks in mind, so the harder version is tasks I didn’t pick.


Key ideas

  • Self-development needs the agent to write its own code, not borrow it from a teacher.
  • Writing in small, checked steps made a teacher more reliable. Run each step on real input and look at the result: 13 of 15 host-checked, with one failure the logs don’t explain.
  • Code can be built by search, with no language model. 29 building blocks, a world model learned from 88,712 executions, and 13 of 13 small tasks solved and host-checked.
  • Imagine a lot, run a little. For “average”: 40,885 imagined steps, 4,213 programs survived, 80 run for real, 0.9 seconds.
  • Traces of running code are the raw material for the next stage. 61,611 programs and 982,893 traced lines, including the agent’s own tools and 4,352 errors.
  • The neural code world model is not there yet. Early checkpoints get about 2% of single steps exactly right on held-out programs.
  • The test ahead is whether experience compounds: blocks learned from traces solving tasks hand-written blocks can’t.

Sources: Balog, Gaunt, Brockschmidt, Nowozin and Tarlow, “DeepCoder: Learning to Write Programs”, ICLR 2017, arxiv.org/abs/1611.01989. Checked October 2026.

Related: A Terminal That Learns What Commands Do, Tests Are How You Talk to the AI, and Treat the LLM Like an Unreliable API.