Self-Improving Loop¶
OLAV is designed for local small models — and small models repeat their mistakes unless something keeps notes for them. OLAV's answer is a three-layer feedback architecture that learns from its own usage without any model fine-tuning, training run, or operator intervention.
This page explains the layers (L1 → L2 → L3), how they compose, and the experimental evidence that proves they actually steer model behaviour.
Feature Claims
| ID | Claim | Status |
|---|---|---|
| C-L3-L1-CAPTURE | OperationalEventCapturePlugin auto-captures write-class tool calls as operational_event memory |
✅ R98 |
| C-L3-L2-EXTRACT | pattern_extractor distils N samples into reusable expert_knowledge pattern via LLM |
✅ R98 |
| C-L3-DIRECTIVE | Directive *.guide.yaml files prime memory via olav kb import-guides |
✅ R98 |
| C-L3-AUTORECALL | Memories auto-injected into model context via AutoRecallMiddleware |
✅ v0.15+ |
What "self-improving" actually means here¶
It does not mean OLAV trains the model behind the scenes — that would be expensive, slow, and risky. It means:
- Every successful tool call is recorded as a memory.
- Recurring patterns are distilled into directive memories by a curator.
- All those memories get automatically injected into the prompt next time something similar happens.
After a week of use, your OLAV deployment is materially smarter than a fresh install — same model, same code, richer memory.
The three layers¶
┌───────────────────────────────────────┐
USER QUERY ────────────────┤ AutoRecallMiddleware │
│ (before each model call, pull top-K │
│ relevant memories, inject into prompt│
└────────┬──────────────────────────────┘
│
▼
┌──────────────────────┐
│ AGENT + LLM │
│ emit tool_calls │
└────────┬──────────────┘
│
┌──────────────────────────────┼──────────────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ L1 capture │ │ Other middleware │ │ tool exec │
│ (after each │ │ (audit, query │ │ + result back │
│ agent run) │ │ pattern, etc) │ │ to LLM │
│ │ │ │ └─────────────────┘
│ Walks │
│ messages, │
│ captures │
│ write-class │
│ tool calls │
│ as │
│ operational_ │
│ event │
│ memories │
└──────┬───────┘
│
│ accumulates ≥ N similar events
│
▼
┌────────────────────────────────────────────────┐
│ L2 extract (on-demand or scheduled) │
│ │
│ pattern_extractor.extract_operational_patterns │
│ (groups by tool, calls LLM once per group, │
│ writes expert_knowledge memory back) │
└─────────────────────────────────────────────────┘
│
│ next AutoRecall hit
▼
┌──────────────────────┐
│ Agent now sees both │
│ the directive AND │
│ concrete precedent │
│ AND a distilled │
│ pattern │
└──────────────────────┘
L1 — write-class capture (always-on, hot path)¶
olav.plugins.middleware.operational_event_capture.OperationalEventCapturePlugin runs as a builtin middleware. After every agent run it:
- Walks the message thread for AIMessage → ToolMessage pairs where the tool name matches a write-class verb (register, deploy, push, save, destroy, write, record, emit, export, ingest, stop, create, update, delete, exec).
- Redacts secrets in the args / result (token, password, api_key, secret, credential, auth_header) before persisting — values are replaced with
<redacted>. - Skips failed calls (
status: error) so the store doesn't accumulate broken examples. - Writes one
operational_eventmemory keyed by SHA256 of the call summary — so re-running the same op doesn't duplicate.
Performance: one embedding call + one LanceDB row per write tool call, sub-second on Ollama-served local embeddings.
L2 — pattern extraction (on-demand, batchable)¶
olav.core.memory.pattern_extractor.extract_operational_patterns(scope, min_samples, window_days) is a Python helper. It groups operational_event memories by tool name; for any group with at least min_samples events in the window, it calls the chat model once to abstract a reusable pattern, and writes the result back as expert_knowledge.
Run it on demand or schedule it nightly:
Or invoke from a curator agent via execute_skill_script — exactly the same code path. This is deliberately not a middleware — LLM calls are expensive, batchable, and need to be auditable, all of which argue for explicit invocation, not hot-path injection.
In Demo7 testing, six register_service events from prior runs distilled into eight bulleted conventions:
- Action is consistently
register_servicerequiring four arguments:name,kind,endpoint, andtoken_env.- Naming convention follows
{kind}_{suffix}…- Authentication is mandatory via the
token_envargument…- Variance flag: Host addressing is mixed (some
localhost, some192.168.100.x)…
That distilled pattern is now the top hit when an agent encounters a new service registration request.
Failure-driven constraints (reflection)¶
L1/L2 learn from successes. A parallel path learns from failures: after every run the audit middleware fires trace_learner in the background (fire-and-forget). It reads failed/cancelled runs from audit.duckdb, asks the LLM to distil one-line constraints ("don't call X when Y holds"), and writes them as reflection memories with scope=global and a 30-day TTL (ADR-0015). AutoRecall then surfaces them to every agent through a reserved reflection quota slot — a mistake made under one agent becomes a guardrail for all. Trigger a review on demand with /trace-review [hours] [limit].
Wired end-to-end 2026-06-13
This loop was write-only for months: constraints were written with scope=shared:audit while recall queried scope=global, so no agent ever read them back. It now writes scope=global with a real embedding and a reserved recall quota.
The reflector agent — scheduled self-reflection¶
trace_learner (above) learns from structured audit.duckdb failures. The reflector (admin/reflector sub-agent) complements it: it also mines the plain-text logs, and adds a code-proposal lane. It runs daily via the reflect cron job at 4am, activated with olav cron enable reflect.
Its anti-hallucination core is scan_error_signatures. Instead of loading megabytes of log text into a small model's context (the api_server.log alone can be ~200 MB+), it returns a bounded error-signature histogram:
- Pull recent errors from
audit.duckdb+ tail-scan the last ~2 MB of each.olav/logs/*.log. - Normalize each message — strip timestamps / ids / numbers / paths to placeholders — into a stable signature.
- Group + count, and return the top-K signatures (K by tier: 15 / 30 / 60), each with one truncated example and a
truncatedflag.
The agent sees "ERROR X ×47, example: …" — precise, capped, and un-inventable. It then triages each signature into one of two lanes:
- KB lane — a one-line operational lesson → written directly as a
reflectionmemory (record_reflection;scope=global, 30-day TTL, quota 1 — self-limiting and reversible); OR a permanentusage_guide→ written as a draft to.curator_drafts/for human (HITL) commit via memory-curator (propose_guide_draft). - Code lane — a real code defect → a proposal (root cause + suggested fix + optional patch) written to
exports/reflections/(draft_code_fix). It never edits source, runs git, or applies anything — a human reviews.
Reflection writes are semantically de-duplicated
A new lesson within L2 distance 0.3 of an existing reflection is skipped, so daily runs over the same recurring errors don't pile up near-identical lessons.
L3 — cross-team / governance (enterprise extension)¶
The data infrastructure for L3 — federated LanceDB, scope-aware aggregation, compliance-grade redaction, audit trail of who-saw-what — is in place. L3 is the natural enterprise differentiator: aggregating L1+L2 across teams, deploying compliance-tested directive bundles per regulated vertical (telco, fintech, healthcare).
OSS distribution ships L1+L2 by default. L3 is opt-in business, not a fork.
Plus: explicit directives (zero-LLM steering)¶
Sometimes you want to encode a rule before the system has any examples to learn from. Drop a *.guide.yaml under <workspace>/<agent>/guides/, then:
The guide is now a usage_guide memory with scope=global (if under core/guides/) or scope=<agent> (otherwise). AutoRecall surfaces it on relevant future queries.
This is how teams encode their conventions ("never inline tokens", "always validate IP plan before write_profile", "use H2 sections in audit reports"). See Extending Memory for the full how-to.
Experimental evidence: it actually works¶
Demo7 Chapter 8 (2026-04-28) tested whether the architecture really steers a small model. Same vague user prompt in every run:
"Write a bash script to backup running-config from all routers. Use real device names/IPs from the database. Save to exports/scripts/. Include --dry-run + error handling."
| # | Backend | Memory state | Result | Wall-clock |
|---|---|---|---|---|
| 1 | qwen3.6:27b (local) | clean | ❌ narrate-only | 2:51 |
| 2 | qwen3.6:27b (local) | clean (directive prompt by user) | ✅ 118-line script | 2:26 |
| 3 | qwen3.6:27b (local) | clean (max_tokens 16000) | ❌ narrate-only | 2:44 |
| 4 | qwen3.6:27b (local) | clean (SKILL.md hardened) | ❌ narrate-only | 1:52 |
| 5 | deepseek-v4-flash (cloud) | clean | ✅ 126-line script | 2:52 |
| 6 | qwen3.6:27b (local) | directive guide + L1 precedent | ✅ 150-line script | 3:40 |
Same model, same prompt, same SKILL.md across runs #1, #3, #4, #6. The only thing that flipped #6 from "narrate-only" to "150-line bash output" was the memory content (one directive guide + one L1 capture from the deepseek run #5).
That's the empirical proof. Memory injection — not bigger context, not stronger prompts in SKILL.md — is what makes a 27B local model behave correctly on free-form requests.
Decay and pruning¶
Memories aren't immortal. The schema includes access_count (incremented on every recall hit) and created_at so a curator can:
- Demote / drop entries with zero access over the last N days.
- Keep recently-cited memories at higher confidence.
- Promote operational events into expert patterns once a count threshold is reached.
These curators are pluggable — sensible defaults ship; teams can add their own.
Operating principles¶
- Memory is the prompt programming language. Behaviour rules live in memory, not code. Adding/removing/tuning them is text editing, not refactoring.
- L1+L2 grows the store from observation. You don't have to anticipate every convention — many emerge as you use the system.
- Directives kickstart what L1+L2 can't infer up front. "Don't inline tokens" is hard to learn from observation; easy to write as a guide.
- Per-scope isolation prevents pollution. Team A's memories don't leak into Team B's agent. Same code, different memory, different behaviour.
- Auditability by default. Every memory has origin, confidence, scope, timestamp.
olav_recall_memoryshows what surfaced for any given query.
Where to next¶
- Memory Architecture — table schema, categories, AutoRecall internals
- Extending Memory — practical guide-authoring walk-through
- Knowledge Base — document import flows for the same store