On Monday you ask a coding agent to run your Python tests. It tries
pytest, hits ModuleNotFoundError, inspects the repo, and works out
the fix: python -m pytest from the repo root, so the package lands on
sys.path. Tests pass. On Tuesday you open a fresh chat and ask for
the same thing — and it runs the wrong command again, hits the same
error again, and spends the same tokens rediscovering the same fix.
Nothing carried forward. And every repeat investigation is not just wasted spend; it is another roll of the dice in which the model might derive a wrong answer this time. An agent without memory can complete today's task. It just cannot get better at tomorrow's.
The vocabulary: agents, harnesses, and four kinds of memory
Before fixing the problem, it is worth being precise about the machine. An agent is a system that takes in information from its environment, reasons about what to do next using an LLM, and acts on that environment through tools. The LLM is the raw reasoning power; everything around it is the harness — the scaffolding that assembles the context the model sees, executes the tools it requests, records the results, and calls the model again. That cycle — request, inference, tool call, result appended to context, repeat until done — is the agent loop. Put a model in a harness and you have an agent. Give the harness nothing that survives the session, and you have the Monday/Tuesday problem.
What survives the session is memory, and it comes in four kinds:
- working memory — the messages and tool results in play right now: the context window of the current run;
- episodic memory — records of previous runs: what was tried, what failed, what fixed it;
- semantic memory — facts about the world the agent operates in; for a coding agent, the structure of the codebase itself;
- procedural memory — the workflows the agent has learned: how to do things, kept as reusable procedures.
The raw material for the last three is the trace: the complete record of one run — files inspected, actions taken, errors hit, corrections made, outcome reached. Traces are what an adaptive agent refines into memory.
One more distinction, because it decides where you spend money. An agent can adapt in exactly two spaces. Token space changes what the model sees — better skills, better retrieved context. It is cheap, fast, and reversible. Weight space changes what the model is — fine-tuning. It is none of those things. So the working rule: exhaust token space first, and touch the weights only for what context cannot reach.
The companion project
To make this concrete I built adaptive-agent-lab, a small working mini-project — "the project" from here on — with Python, Neo4j, and Qwen models end to end. The framing follows the DeepLearning.AI short course Building Adaptive AI Agents (taught with Oracle); the course builds on an Oracle stack, and the project re-implements the ideas from scratch on Neo4j.
The architecture in one picture:
A few structural decisions worth naming.
One database, three memories. Neo4j holds all three long-term
memory types as one property graph in different shapes: traces as
(:Episode)-[:HAS_STEP]->(:Step) chains (episodic), versioned skills
as (:Skill) nodes with provenance edges back to the episodes that
produced them (procedural), and the codebase as files, functions, and
their relationships (semantic). Working memory is not stored at all —
it is assembled, per task, by the final stage, which pulls the best
approved skill and the graph-ranked file hints into the prompt.
Qwen plays two roles. A chat-sized Qwen (served locally through Ollama, or any OpenAI-compatible endpoint) does the token-space work: it is the induction engine that drafts skills and the model that answers the final prompt. A separate, deliberately small Qwen3-0.6B is the weight-space subject — the base model that gets a LoRA adapter fine-tuned on top.
Numbered stages, inspectable intermediates. The project is ten
scripts, 01_seed_skills.py through 10_agent_demo.py, each doing
one thing and writing an artifact you can open before the next stage
runs — parser output lands in JSON files, the training set is a JSONL
you can read, retrieval results carry their full score breakdown.
There is no agent framework underneath; the package is plain Python
modules (skills/, ckg/, finetune/) with the Cypher written at
the call sites.
The rest of the post walks the three adaptation layers this architecture implements, ordered by cost.
Layer 1: traces become skills, behind a human gate
The first layer refines episodic memory into procedural memory. An
induction engine — an LLM with a fixed contract — reads every
episode recorded for a topic alongside the currently active skill, and
drafts an improved, numbered procedure that folds the proven fixes
into the steps. The project's sample traces contain the pytest
failure three times; the proposed v2 of the run-the-tests skill
starts with the command that actually works.
The important design decision is what happens next: nothing. The proposal is saved as pending. A skill only becomes behaviour after a human compares the versions and approves — and a rejection must carry a reason, which the engine reads on its next attempt.
Why insist on the gate? Because approval is the moment a proposal turns into behaviour the agent will retrieve on every matching task from then on. That is high leverage in both directions. A bad skill means the agent repeats a mistake forever. Worse, traces are data the agent collected from the world — tool output, file contents, error text — and a deliberately planted instruction in that data could otherwise be laundered into permanent agent behaviour. The review gate is the defence, and it also gives every skill in the box an owner.
Layer 2: retrieval is the bottleneck, so structure the knowledge
The second layer builds semantic memory. Here is the underappreciated fact about coding agents on real codebases: writing the code is not the hard part. Finding the right files to change is. The default tool for that — keyword and regex search — matches tokens and is blind to structure. Ask it to "fix the cache issue" and it finds the files containing the word cache, and misses the caller two hops away that breaks when you change them.
Codebases, though, are full of relationships that are sitting there waiting to be extracted:
- imports — file A imports file B; change B and A may be affected;
- calls — a function in one file calls a function in another;
- co-edits — files that keep changing together in git history tend to change together in the future.
That is a property graph, and it is where Neo4j earns its place. The
project parses a repository with Python's ast module and git log —
each parser writing its result to a plain JSON artifact you can read
before the database sees anything — and loads:
(:CodeFile)-[:IMPORTS]->(:CodeFile)
(:CodeFile)-[:CONTAINS]->(:CodeFunction)
(:CodeFunction)-[:CALLS]->(:CodeFunction)
(:CodeFile)-[:CO_EDITED {count}]->(:CodeFile)
Retrieval is two explicit steps. First, anchor: embed the query and every node label, and take the closest nodes as starting points. Second, walk: run personalised PageRank from the anchors across the graph. The distinction matters — a plain fixed-radius traversal scores every node two hops out identically, while PageRank gives each node a score shaped by both distance and connectivity. The project's implementation is a visible power iteration over a dict, not a library call, and the final ranking keeps the PageRank score, the cosine similarity, and the blend for every node, so you can always answer why a file was suggested.
On easy queries — where the answer shares words with the question —
the graph merely matches keyword search. The payoff is the multi-hop
case: "when get_chat_archive runs, which internal function does it
call?" Keyword search returns a lexical lookalike; the graph walks the
call edge to the right answer. The course's own benchmarks, injecting
graph-ranked file hints into a coding agent's context on HTTPie and
Django tasks, measured roughly 10–18% improvements in time-on-task,
steps to first correct edit, and tokens — modest per task, but it is a
discount that compounds on every task. And the maintenance loop is
cheap: when new commits land, you re-parse and MERGE the new nodes
and edges into the existing graph — an append, not a rebuild — on
whatever cadence the codebase's rate of change deserves.
Layer 3: when context cannot reach it, touch the weights
Some behaviours will not stick through prompting: a persona, a strict output format, a refusal policy. For those, the last resort is fine-tuning — done in a way that does not destroy the base model.
LoRA freezes the original weights W and trains two small
matrices whose product is added on top: W' = W + BA. You train
roughly 1–5% of the parameters. Too few and the behaviour never
sticks; too many and you overwrite what made the base model good —
catastrophic forgetting. QLoRA makes the economics workable by
loading the frozen base in 4-bit precision during training: the 32-bit
weights are scaled into sixteen 4-bit buckets, at the price of a small
reconstruction error, and a 600M-parameter model fits in about a
gigabyte of memory.
The project fine-tunes Qwen3-0.6B — deliberately small — into an unmissable persona: a super polite coding assistant, trained on synthetic question/gracious-answer pairs. The tokenisation is written out by hand, prompt tokens masked from the loss so the model is trained only on how it answers. The result is an adapter of a few dozen megabytes sitting beside a multi-gigabyte base model: same weights underneath, one small delta on top.
Politeness is a toy, but the deployment pattern around it is not: a router in front of the model decides, per query, whether the base model or an adapter answers. In the project it is a transparent rule table — frustration and courtesy route to the polite adapter, factual questions go to the base — and every decision names the rule that fired. Swap "polite" for "house output format" or "refuse these topics" and you have the production version.
The payoff, and what this does not prove
The final stage assembles working memory from everything the other layers built: given a task, the agent retrieves the best approved skill from the box, retrieves graph-ranked file hints with their score breakdown, injects both into the prompt, and answers through Qwen. Tuesday's agent starts where Monday's finished.
The honest caveats. The benchmark numbers above are the course's measurements, not mine — reproduce them before you budget against them. The politeness adapter demonstrates the mechanics of weight adaptation, not a business case. The regex router is a placeholder for a learned classifier. And the human review gate is a bottleneck by design — that is the feature, but it means skill quality moves at the speed of your review discipline, and a skill box nobody reviews is a skill box nobody should trust.
Everything in the project is deliberately explicit: numbered stages, each writing a JSON artifact you can open; Cypher at the call site so you can paste it into Neo4j Browser; the PageRank iteration in plain Python. There are frameworks that would collapse the whole thing into a dozen lines, and they would teach you a dozen lines' worth. When the point is to understand how adaptation works, the magic is exactly the part you want to see.
The code, sample traces, seed skills, and the full setup walkthrough are in the companion repo. Point the graph builder at one of your own repositories and try the multi-hop queries — that is where it stops being a diagram and starts being a tool.