Meta-Engineering AI Systems · Part 1

Meta-engineering AI systems: from traces to better memory

This article is for engineers who want an agent to find out why their AI system fails and to propose a fix. The example is a small memory tool that answers a question about where a user lives, and the answer is wrong. You will see how a change to that tool is proposed, tested on 20 prepared scenarios, and judged before anyone adopts it. You will also see how to check whether a plain script that tries every setting would have found the same fix without an LLM. In the companion experiment, an LLM proposed the changes and ordinary Python tested them.

I use meta-engineering for the design of that improvement process. It comes down to four decisions. What may the proposer, the person or program that suggests changes, change? Which results count as an improvement? How does a result shape the next suggestion? Who decides to adopt a change? The same four decisions apply whether the proposer is an LLM, a person or a parameter search.

The wrong-city failure

The memory tool stores facts about a user, Ada, and answers questions about them. One test, the future-move scenario, sends it four events:

  1. Save city = Berlin, effective January 1.
  2. Save city = Paris, received January 3 but effective January 10.
  3. Ask for the city on January 5. Expect Berlin.
  4. Ask for the city on January 12. Expect Paris.

The original tool answers Paris both times. On January 12 that is correct. On January 5 it is wrong: Ada has not moved yet.

A timeline from January 1 to January 13: Berlin is valid from January 1 up to January 10; Paris is received on January 3 but valid only from January 10. Asked for January 5, the original retrieval answers Paris, which is wrong, and the date filter answers Berlin; asked for January 12, both answer Paris.A timeline from January 1 to January 13: Berlin is valid from January 1 up to January 10; Paris is received on January 3 but valid only from January 10. Asked for January 5, the original retrieval answers Paris, which is wrong, and the date filter answers Berlin; asked for January 12, both answer Paris.

Each test run leaves a trace: a saved record of every event sent to the tool, the records it stored and the answers it gave. The saved trace for this scenario shows that both records have the right validity dates. Berlin is valid from January 1 until January 10, and Paris from January 10 with no end date. The fault is in retrieval: it ranks the more recently received Paris fact ahead of Berlin without checking which one was valid on the requested date. Storing the future move was correct.

So the Paris fact must stay, and retrieval needs a date filter. That diagnosis does not yet justify adopting a filter. The same retrieval code answers questions about other subjects, handles deletions and must keep users’ records separate. A change that fixes January 5 while breaking those cases makes the memory tool worse. The improvement process has to detect that before a new version reaches users.

Two loops

An agent application runs a loop while it serves a user. In this runtime loop, it chooses an action, calls a tool, reads the result and continues the user’s task. An agent using the memory tool could receive the wrong-city answer there. This experiment calls the tool directly with fixed test events.

Fixing the failure needs a second loop, which runs between versions of the application. This improvement loop starts from evidence of a failure, such as the January 5 answer, a record changed when it should not have been, or a cost that is too high. The proposer suggests a candidate: one specific version of a change to the target, the part of the system being improved. Here the target is the memory tool, and a candidate is a new set of its settings. A runner executes the candidate on a fixed set of test scenarios, and an evaluator checks the results against the rules those scenarios encode. The loop is complete only when a result changes the next proposal or the decision to adopt a change.

The diagram shows the general application design, where the target can be any part of the application. Engineers often group those parts into four overlapping areas. Prompt engineering changes the instructions. Context engineering changes what information the model receives, including memory. Loop engineering changes how the agent acts, retries and stops in its runtime loop. Graph engineering changes how steps or agents connect and route work. Meta-engineering treats a choice in any of the four, such as the wording of a prompt or the number of retries, as a setting an experiment can change. It judges every candidate by the whole system’s answer quality, cost and required behavior, such as never showing one user’s data to another.

General application design: an agent runs a runtime loop (choose action, call tool, read result, continue task) and has four areas a change can touch: prompt, loop, graph and context, where the memory tool is this article's target. Below it, the improvement loop runs between versions: a trace gives the evidence, a proposer suggests a candidate, a runner and an evaluator test it, the result shapes the next proposal, and a person decides whether to adopt the change. The companion tests the memory tool directly.General application design: an agent runs a runtime loop (choose action, call tool, read result, continue task) and has four areas a change can touch: prompt, loop, graph and context, where the memory tool is this article's target. Below it, the improvement loop runs between versions: a trace gives the evidence, a proposer suggests a candidate, a runner and an evaluator test it, the result shapes the next proposal, and a person decides whether to adopt the change. The companion tests the memory tool directly.

Two recent projects already use a coding agent as the proposer. In Karpathy’s autoresearch, the agent edits one training file and runs it under a fixed time budget. Each attempt is recorded as kept, discarded or crashed, and the evaluation code stays in a separate file the agent does not edit. Meta-Harness, a March 2026 preprint, lets the proposer read the source code, scores and traces of earlier candidates. It uses them to improve the code that decides what an LLM application stores, retrieves and shows its model. This article uses a much smaller target, the memory tool, so that every step can be inspected.

Limit what the proposer can change

The LLM never runs inside the memory tool. It reads failed traces and the evaluation results, then proposes a new configuration: values for the tool’s settings. Python tests that configuration.

The memory tool itself is ordinary, deterministic Python. It receives structured facts, stores or rejects them, and returns a value for a query. It does not extract facts from conversation or write a natural-language answer, so this loop cannot fix a failure in fact extraction. Each stored fact has an owner, a source event and a validity interval, following the stored-state rules in Schema-Guided Agent Memory.

Each incoming fact also carries a confidence score, written by the test author. The tool stores a fact only if its score is at least min_confidence. The scores are part of the test data, not probabilities measured from a model. For example, an uncertain delivery = courier fact scored 0.4 is rejected at the original threshold of 0.7, but stored at 0.2.

The Paris record after the second write shows how a record keeps its history. observed_at is the day the tool learned the fact, and valid_from is the day it becomes true (excerpt):

{
    "id": "m002",
    "key": "city",
    "value": "Paris",
    "valid_from": "2026-01-10",
    "valid_to": null,
    "observed_at": "2026-01-03",
    "source_event_id": "future-move:event-2",
    "supersedes_memory_id": "m001"
}

source_event_id points to the event that supplied Paris, and supersedes_memory_id links it to the Berlin record m001. The writer also has fixed rules for conflicts. If a later write says Rome, also effective January 10, the writer rejects it and leaves Paris unchanged. A repeated Paris for the same date reuses the record, and an update with an earlier effective date is rejected. The proposer cannot change these rules.

The proposer can change five existing settings:

SettingWhat changing it does
min_confidenceChanges the minimum confidence score required to store a fact.
filter_entityRestricts retrieval to the requested subject, such as home rather than work.
time_awareRestricts retrieval to facts valid on the requested date.
deduplicateReuses an identical active fact instead of storing another confirmation.
top_kSets how many of the best-ranked records the tool considers for the answer.

One proposal changes at most two settings; min_confidence ranges from 0 to 1 and top_k from 1 to 8. The agent cannot edit any code, test or expected answer, and it cannot approve its own change. Its output is data that Python validates, not code to execute. The configuration contract and proposal validator define that limit in code.

Because this proposer can only send settings, it cannot change the evaluator. The evaluator does run inside the same Python process as the memory tool, though. An agent allowed to write and run its own code could therefore change the checks. Such an agent needs the evaluator in a separate process with its own file and network access rules before its results can be trusted.

The scoring rule

For the city story, the test must ask both questions: Berlin on January 5 and Paris on January 12. Testing only the first answer would reward a tool that simply discards the future fact. In the companion, a scenario is one complete story of write, query and sometimes delete events, like the four events above. Each candidate runs the same 20 prepared scenarios, each from an empty memory. The original configuration passes 13 of them.

A scenario passes only when every expected answer is correct and no data rule is broken. The evaluator checks three data rules, called hard checks: one owner never sees another owner’s facts, a deleted fact is gone, and an input marked as prohibited is not stored. It also counts the records the tool stored.

To choose between candidates, the selection rule first discards any candidate that fails a hard check. Among the rest, it prefers the one that passes more scenarios. If two pass the same number, it prefers the one that stores fewer records. So a change that only reduces storage can win when the answers stay the same, but never against a change that answers more scenarios correctly.

One improved number is not enough to accept a change. Consider lowering min_confidence from 0.7 to 0.2. More useful facts get stored, such as a preference that arrived with a low score. The uncertain delivery = courier fact gets stored too, and it replaces a confirmed value. In a hand-prepared comparison, this change fixed one scenario but broke four, so success fell from 19/20 to 16/20 while every hard check still passed. The share of useful facts stored went up, and the final answers got worse. This candidate was not an agent proposal in the campaigns below; it shows why the score counts complete scenarios.

The checks have limits. The test supplies owner IDs directly, so owner isolation here is not an authentication test. The prohibited-write check only rejects inputs the test already marks as prohibited; it cannot find a malicious instruction hidden in ordinary text. And the proposer can see all 20 scenarios, so none of them works as a hidden test set. A passing score tells us what happened on these cases, not how an unknown application will behave.

Give each proposal a testable reason

The experiment ran as campaigns. A campaign is one attempt to improve the original configuration, starting with a fresh agent history. Each iteration is one model call in which the agent proposes a change or decides to stop. In the first request, the agent receives the current settings and their meanings, the baseline’s results, traces from the failing scenarios, the changes it may propose, and, in later iterations, earlier proposals and their results.

Every response must fill in the same five fields. This format follows Schema-Guided Reasoning (SGR): the response is a decision record that a reviewer can check field by field.

FieldWhat a reviewer can check
observationsWhich supplied scenario and event support the proposed change?
hypothesisWhat rule appears to cause the problem?
predicted_effectWhat should improve when the change is tested?
actionIs the agent proposing a change or stopping?
patchWhich permitted settings should change?

The request enforces that shape with a strict JSON Schema through OpenAI Structured Outputs. The schema constrains the response shape; it does not prove the hypothesis. Python then checks whether the cited events exist in the evidence it supplied, whether the patch stays within the allowed settings, and whether that configuration has already been tested. A response can match the schema and still fail these checks.

In the first recorded iteration, the agent cited the future-move failure and a second failure, in which a fact about Ada’s work address answered a question about her home. It proposed enabling both the subject filter and the date filter:

{ "filter_entity": true, "time_aware": true }

In the implementation, these settings remove ineligible records before ranking. The subject filter drops facts about other subjects, so a work fact cannot answer a home question. The date filter keeps only facts valid on the question’s date. A fact is valid from its start date up to, but not including, its end date:

def valid_at(item: dict, at: str) -> bool:
    return item["valid_from"] <= at and (
        item["valid_to"] is None or at < item["valid_to"]
    )

Paris starts January 10, so for the January 5 question only Berlin remains. On January 12, Paris is valid and Berlin is not.

Python ran that configuration on all 20 scenarios. Success rose from 13/20 to 19/20, and the hard checks passed. The campaign changed both filters in one step, so it measured only their combined effect; the single-setting reruns below separate them. The runner kept this configuration as the starting point for the next proposal and returned its results to the agent.

One iteration: the agent reads settings, results, failing traces and the changes it may propose, and writes a decision record with five fields. Python checks the cited events, the allowed settings and repeats, returns a rejected record with its error, runs a valid one on 20 scenarios and applies the selection rule. The result goes back to the agent, experiment history keeps every step, and a person decides adoption outside the loop.One iteration: the agent reads settings, results, failing traces and the changes it may propose, and writes a decision record with five fields. Python checks the cited events, the allowed settings and repeats, returns a rejected record with its error, runs a valid one on 20 scenarios and applies the selection rule. The result goes back to the agent, experiment history keeps every step, and a person decides adoption outside the loop.

After the filters, one scenario still failed, and some stories stored the same confirmation twice. The first campaign made four decisions in total:

DecisionEvidence and proposed changeMeasured result on the same 20 scenarios
1Enable subject and date filtering after wrong-subject and wrong-date answers13/20 → 19/20; kept as the next starting point
2Deduplicate repeated language = German confirmations19/20; mean stored records 1.70 → 1.60
3Lower min_confidence from 0.7 to 0.6 for a useful preference scored 0.620/20; mean stored records 1.65
4Stop after no further supported proposalNo new configuration tested

At 0.6, the useful preference is stored while the uncertain courier fact scored 0.4 stays below the threshold. That explains the third change on these prepared inputs; the scores themselves were supplied by the test author. The stop decision means the agent found no further change it could support from the feedback it saw. It does not show that this configuration is the best one possible.

Validation has also rejected a response. In a separate development run, an agent cited a candidate ID as though it were a scenario event. The response matched the JSON schema, but the reference did not exist in the supplied event evidence. Python rejected the proposal before testing it and returned the validation error. None of the three recorded campaigns below had such a rejection.

If an evaluated patch makes the score worse, the runner keeps the previous starting point and records the failed attempt. A scripted test exercises that path; the three live campaigns did not. Keeping the rejected result in the history gives the proposer a reason to choose a different next change instead of repeating the same guess.

Repetition, cost, and adoption

The memory tool is deterministic: rerunning the same configuration on the same scenario gives the same answer. The uncertainty is in what the agent chooses. The study therefore ran the whole search three times, each from the 13/20 baseline with fresh agent history and at most four model decisions. All three campaigns chose the same three patches, reached 20/20 on the same scenarios, and then stopped. That is 12 model calls, nine evaluated proposals, and three stop decisions.

For the September 11, 2026 study, the proposer was gpt-5.6-luna at low reasoning effort. Priced with the published model rates, the recorded token usage comes to about USD 0.04 for the 12 calls, against a configured USD 0.50 budget. That is an estimate from recorded usage, not an invoice, and it excludes development time.

These repeated results show that this model followed one useful path on this prompt, target and set of scenarios. They do not show that a new memory workload will pass. The companion’s frozen campaign report and checksummed evidence archive contain the recorded requests, responses, validations and results.

A configuration the runner keeps is only the starting point for the next experiment. It is not a release. In the companion reports, adoption is still waiting for human review, and the campaigns deployed nothing. A reviewer would still need to check the change against their own workload, access rules, cost target and rollback plan.

The experiment also produces records of its own, and they need different handling from Ada’s facts. The memory tool stores her city. The experiment history stores each request, proposal, validation result, test version and result, with the hashes needed to explain a choice later. In this synthetic study those records can be public. In a real application, deleting a user’s fact from the live memory store would not erase copies in experiment logs or backups. A retention and deletion policy must cover those records separately.

User memory and experiment history side by side. Deleting Ada's city for the same tenant and account removes both city versions from user memory: m001, Berlin, and m002, Paris. A copy of m002 remains in a trace inside the experiment history and in backups.User memory and experiment history side by side. Deleting Ada's city for the same tenant and account removes both city versions from user memory: m001, Berlin, and m002, Paris. A copy of m002 remains in a trace inside the experiment history and in backups.

Did this search need an LLM?

The campaigns show that the loop works: an LLM read traces, proposed settings and changed its next proposal after feedback. They do not show that an LLM was the right proposer for this problem. To check, I reran the unchanged memory tool on the same 20 scenarios with one setting changed at a time, using the companion’s settings grid script. These runs make no model calls, and each configuration needs only one run because the tool is deterministic.

ConfigurationScenarios passed
Baseline13/20
Subject filter only16/20
Date filter only16/20
Deduplication only13/20
min_confidence = 0.6 only14/20
Both filters19/20
Both filters, deduplication, min_confidence = 0.620/20

Each filter fixes three scenarios on its own, and together they fix six. Deduplication changes no answer; it only reduces stored records.

I then tested every combination. The facts in the scenarios have six confidence scores: 0.25, 0.3, 0.4, 0.6, 0.95 and 1. A threshold stores every fact scored at or above it, so 0.45 and 0.55 store exactly the same facts. One threshold value per gap between neighbouring scores is enough, and six values cover every distinct write behaviour on these scenarios.

With those six thresholds, three on/off settings and eight top_k values, there are 384 configurations. The script runs each one on the 20 scenarios and finishes in under a second.

Sixteen configurations pass 20/20. All of them enable both filters and use a threshold above 0.4 and at most 0.6; top_k never changes the result. Ranked by the selection rule above, the best configuration is the one the agent found: both filters, deduplication and min_confidence = 0.6, with 1.65 stored records per scenario. The saved report lists every result with the source hash.

In the figure, each row is one range of thresholds, the filled dots are the facts it stores, and the marked row holds all 16 passing configurations.

A matrix of the six threshold ranges against the six confidence scores, with filled dots for the facts each range stores. The threshold 0.2 is in the first row and stores the courier fact scored 0.4; the original 0.7 is in the fifth row; the agent's 0.6 is in the marked fourth row, above 0.4 and at most 0.6, where all 16 passing configurations sit. Below, 6 × 2 × 2 × 2 × 8 gives 384 configurations, and 1 × 1 × 1 × 2 × 8 gives the 16 that pass.A matrix of the six threshold ranges against the six confidence scores, with filled dots for the facts each range stores. The threshold 0.2 is in the first row and stores the courier fact scored 0.4; the original 0.7 is in the fifth row; the agent's 0.6 is in the marked fourth row, above 0.4 and at most 0.6, where all 16 passing configurations sit. Below, 6 × 2 × 2 × 2 × 8 gives 384 configurations, and 1 × 1 × 1 × 2 × 8 gives the 16 that pass.

This grid is not a fair comparison with the agent. It uses my knowledge of the confidence scores in the test data, and it ignores the agent’s limit of four decisions. It shows only that this search space is small enough to test completely, so it did not need an LLM proposer.

The grid and the agent are two of four ways to split the choice of experiments between people and software:

Four modes ordered by how much of each experiment the system chooses: model comparison, configuration search, component search and agent-led investigation. Each card shows a sketch of the mode, what people choose, what the system chooses and an example. A band above lists what people keep in every mode, and all modes write to one experiment ledger.Four modes ordered by how much of each experiment the system chooses: model comparison, configuration search, component search and agent-led investigation. Each card shows a sketch of the mode, what people choose, what the system chooses and an example. A band above lists what people keep in every mode, and all modes write to one experiment ledger.

  1. Model comparison. You choose the exact models or variants; the system runs and scores them. No LLM proposer is needed.
  2. Configuration search. You choose which settings may vary and their bounds; a grid, random search, or an optimizer such as Optuna picks the values. The grid above is this mode.
  3. Component search. You approve interchangeable components with fixed interfaces, and the search picks components and settings. Scikit-learn’s pipeline search does this with ordinary grid search, so swapping components does not by itself make the process agentic.
  4. Agent-led investigation. An agent reads failures and earlier results, proposes the next change, and adapts after each test, as the campaigns above did. Hybrids exist: DSPy’s MIPROv2 has a model propose instructions and uses Bayesian optimization to search their combinations.

In every mode, people keep the objective, the allowed changes, the evaluator, the budget and the adoption decision. All modes can write to the same experiment history, which the diagram calls the ledger: each candidate and its parent, the exact change and who supplied it, the test version, the result, the cost and any rejection. With that record you can hand the next proposal to an agent, or take it back, without losing evidence. The companion’s runner also accepts a candidate supplied by a person through lab evaluate.

Decide first which of these modes fits your system. If the allowed changes are a few settings with known ranges and each test is cheap, enumerate them or run a standard optimizer before you add an LLM. An LLM proposer can help when choosing a change requires reading varied failure traces, source code and interactions between components, and when the candidates are too many or too unstructured to list. Whether it helps there is a question to measure, not assume.

Apply it to your system

Pick one trace that shows a real failure. Then write down four things before any search starts:

  • which changes are allowed;
  • which task outcomes and forbidden effects the evaluator checks;
  • what each attempt may cost;
  • who may adopt a candidate.

Try the simplest search that covers the allowed changes. Then check whether the result changes the next experiment or a release decision. If it changes neither, the loop has not improved anything.

Optional: inspect and run the companion

You can inspect the recorded study without an API key. Tag v0.2.3 contains the memory tool, the recorded studies and the settings grid. With uv installed, clone it, restore the recorded evidence, verify it, rerun the grid and start a local viewer for the recorded campaigns:

git clone --branch v0.2.3 --depth 1 https://github.com/slavadubrov/meta-engineering-ai-lab.git
cd meta-engineering-ai-lab
uv sync --frozen
uv run --frozen python scripts/fetch_evidence.py
uv run --frozen python -m lab verify artifacts/agent-study-03 --source
uv run --frozen python scripts/settings_grid.py --check
uv run --frozen python -m lab serve --port 8075

Open http://127.0.0.1:8075/web/ to follow a recorded campaign from proposal to feedback. The download is checked against a pinned checksum, and nothing in this block makes a model call. The --check run recomputes the grid and fails if the saved report no longer matches the code.

To run a fresh, paid proposal campaign, save OPENAI_API_KEY in the repository’s ignored .env.local file and choose a new output directory:

uv run --frozen --env-file .env.local python -m lab campaign \
  --live --campaigns 3 --iterations 4 --budget-usd 0.50 \
  --output artifacts/my-agent-study

The explicit --live flag enables provider calls. Inspect artifacts/my-agent-study/report.md for the resulting proposals and costs. The model may take a different path even though Python will score any given configuration the same way. The memory-tool walkthrough shows the prepared JSON inputs, stored records, and four hand-specified configurations if you want to understand the deterministic target without running the agent.

Where the series goes next

The memory tool was small on purpose: five settings and 20 scenarios make every step inspectable. The same size is its limit. A script can test every configuration, so this example cannot show when an LLM proposer is worth its cost.

Later parts of this series use a larger target: the customer-support harness from Designing an agent harness, Part 1 of Building and Evaluating Agent Harnesses. A coding agent will propose changes to that harness, and its results will be compared with simpler search. The improver will not edit itself, and a person will review every adoption.

That harness needs a stronger evaluator first. Seven of its twelve support tasks expect no database change, so an agent that does nothing already passes 7 of 12 state checks. A coding agent scored only by those checks could pass more of them by making the support agent do less.