Skip to content

An agent that learns from its own runs

The scenario. Your assistant keeps making the same mistake. It re-runs a build command that has failed the same way three times. It keeps asking you which staging database to use. It formats reports the way you corrected it on Monday. Every individual run is fine; the sequence of runs never gets better, because each run starts from the same fixed prompt and throws away everything it discovered.

The fix is a loop with two moving parts:

  1. After a run, a cheap, tightly-scoped reviewer agent reads what happened and writes a small number of durable lessons into MEMORY.md.
  2. On a slow heartbeat, a consolidator persona re-reads that memory and merges duplicates, drops stale entries, and keeps it tight.

Lessons written today are part of the agent’s identity tomorrow, because fromDir composes MEMORY.md into the soul at the start of every session.

Reach for it when Skip it when
The agent runs the same class of task repeatedly (deploys, triage, reports) and its mistakes are repeatable Every run is a one-off — there is no pattern to learn
Corrections currently live in your head and you re-type them The knowledge is stable and small enough to just write into SOUL.md once
The agent has a home directory already (a persona, fromDir) The agent is stateless by design and you want it to stay auditable-by-prompt
Runs are expensive enough that repeating a known-bad path costs real money or time Runs are cheap and the failure mode is harmless

Why a separate reviewer beats self-reflection

Section titled “Why a separate reviewer beats self-reflection”

The obvious version is to append “now reflect on how that went” to the end of the same conversation. It is worse on three axes, and understanding why is what makes the design hold up:

Self-reflection (same context) Scoped reviewer sub-agent
Bias The model grades its own transcript while inside it — it has already committed to its reasoning, so it rationalizes rather than critiques A fresh context sees a digest, not the reasoning that produced it. It has nothing to defend.
Cost The reflection turn re-sends the entire transcript at your main model’s price — often the single most expensive turn of the run The reviewer sees a short digest, on a cheap model, under a hard token budget
Contamination Reflection output stays in the transcript and steers subsequent turns; a bad review poisons the rest of the session The child’s reasoning never flows back up — task returns exactly one result
Blast radius The reflector inherits every tool the main agent had — including the destructive ones uses scopes the child to exactly one tool: memory

That last row is the security argument, and it is the one people skip. A reviewer’s job is to write a note. Giving it shell access because it happened to be spawned by an agent that had shell access is how a “reflection step” becomes an incident. In toolnexus, scoping is the security model — the child sees only the tools you list in uses.

Define it once, next to the agent it reviews. Three things matter: a soul that demands specificity, a uses list containing only memoryTool(dir), and a hard budget.

the reviewer's soul (inline below, or reviewer/SOUL.md)
You review a single completed agent run and record what should be remembered.
Write a lesson ONLY when it will change behaviour on a future run. A lesson is:
- specific (names the tool, the command, the argument, the file — not "be careful"),
- durable (true next week, not a fact about this one request),
- actionable (states what to do, not what went wrong).
At most TWO lessons per run. If the run went as expected, record nothing and reply
exactly: NOTHING_LEARNED.
Never record secrets, credentials, tokens, personal data, or anything the user asked
you to forget. Use the memory tool with action=add, target=self.
import { agents } from "toolnexus"
const dir = "./personas/ava"
const reviewer = agents.agent("reviewer", {
does: "reads a finished run and records durable lessons",
soulFile: "./personas/reviewer/SOUL.md",
uses: { tools: [agents.memoryTool(dir)] }, // the ONLY tool it can touch
model: "openai/gpt-4o-mini", // cheap: it reads a digest, not a codebase
budget: { maxTokens: 4_000, maxTurns: 3 }, // a review that runs long is a broken review
})

Part 2 — the digest: what the reviewer actually sees

Section titled “Part 2 — the digest: what the reviewer actually sees”

Do not hand the reviewer the transcript. RunResult already carries a structured summary of the run — the final text, every tool call with its error flag, the turn count, and the token usage. That is enough to spot “this tool failed twice with the same args”, and it is two orders of magnitude smaller than the messages array.

function digest(r: RunResult, prompt: string): string {
const calls = r.toolCalls
.map((c) => `- ${c.name}(${JSON.stringify(c.args)}) → ${c.isError ? "ERROR: " : ""}${c.output.slice(0, 200)}`)
.join("\n")
return [
`TASK: ${prompt}`,
`OUTCOME (${r.turns} turns, ${r.usage.totalTokens} tokens): ${r.text}`,
`TOOL CALLS:\n${calls || "(none)"}`,
].join("\n\n")
}

The whole learning step is one extra call after the run you were already making. It is fire-and-forget: the review must never block the user’s answer, and a failed review must never fail the run.

const llm = { baseUrl: "https://openrouter.ai/api/v1", style: "openai", model: "openai/gpt-4o-mini" }
export async function handle(prompt: string) {
const r = await ava.ask(prompt, { toolkit: tk, id: "ava" })
// Fire-and-forget: the user already has their answer.
void reviewer.run(digest(r, prompt), { llm }).catch((e) => log.warn("review failed", e))
return r.text
}

The reviewer’s memory add lands in MEMORY.md on disk immediately. It becomes part of Ava’s soul at the start of her next session — the frozen-snapshot rule, which is what keeps a long-lived persona’s prompt cache warm. See the persona build for how to design session boundaries around it.

Part 4 — the consolidator (the “dream” pattern)

Section titled “Part 4 — the consolidator (the “dream” pattern)”

A reviewer that only appends produces the classic failure: after three weeks MEMORY.md has forty entries, eight of which say the same thing in different words, three of which contradict each other, and all of which are in the prompt prefix of every session. Append-only memory is a slow-motion cost and quality regression.

The fix needs no new API either: a persona whose HEARTBEAT.md tells it to tidy its own memory, started on a slow beat. It reads MEMORY.md through the frozen snapshot, rewrites it with the memory tool, and the next session sees the tidied version.

personas/ava/HEARTBEAT.md — the consolidation rule
On a heartbeat, review your MEMORY.md section:
- If two entries say the same thing, merge them into one clear entry
(memory tool, action=replace).
- If an entry is contradicted by a newer one, remove the older (action=remove).
- If an entry is vague ("be careful with X"), either sharpen it into something
actionable or remove it. Vague lessons are worse than none.
- Keep at most 30 entries. If there are more, remove the least useful.
If nothing needs tidying, reply exactly HEARTBEAT_OK so this beat stays silent.
const ava = agents.fromDir("./personas/ava") // memory tool wired automatically
const nightly = agents.startAgent(ava, { llm }, {
everyMs: 24 * 60 * 60_000, // once a day
onBeat: (summary) => log.info("memory consolidated:", summary), // quiet nights surface nothing
})
user request
┌─────────┐ RunResult ┌────────────┐ memory.add ┌───────────┐
│ Ava │ ─────digest──────▶ │ reviewer │ ──────────────▶ │ MEMORY.md │
│ (run) │ │ (cheap, │ │ (disk) │
└─────────┘ │ memory- │ └─────┬─────┘
▲ │ only) │ │
│ └────────────┘ │
│ │
│ composeSoul at session start (frozen snapshot) │
└──────────────────────────────────────────────────────────────┘
┌────────────────┐ memory.replace / remove │
│ consolidator │ ◀──────────────────────────────┘
│ (heartbeat, │ reads + rewrites
│ daily) │
└────────────────┘

Three shipped primitives, one direction of flow, no new noun: a run produces a RunResult; a scoped agent turns it into a durable note; the soul composition feeds it back on the next session; a heartbeat keeps the notes clean.

Knob Where it lives What goes wrong if you ignore it
What counts as a lesson the reviewer’s soul Truisms. “Be careful with production” is not actionable and never changes a future run.
How many lessons per run the reviewer’s soul (cap it — two is a good start) Unbounded growth; the consolidator can’t keep up with the reviewer.
Reviewer model + budget model, budget on the agent A learning tax bigger than the work. Cheap model, ≤ 4k tokens, ≤ 3 turns.
What the reviewer can see your digest function Secrets and PII persisted into a prompt prefix, forever. Redact here.
Consolidation cadence + entry cap everyMs, HEARTBEAT.md A MEMORY.md that grows until it dominates your per-session token cost.
When memory takes effect your session boundaries (frozen snapshot) “It didn’t learn!” — it did; the write lands next session, not this turn.
Bad lessons git on the home directory No way to undo a wrong lesson. Commit personas/ava/; a bad entry becomes a revert.