Agentic systems

Measuring an agent orchestration pattern

I built a pattern for running AI agents on research work, then spent four weeks measuring it. The catching was done by independent reading — by a session from another project and by an auditor with no stake — rather than by the contracts, gates and automated checks I had built to do it.

An orchestration pattern for running AI agents on research work, and four weeks of measuring it against synthetic datasets built with a known true answer and a trap planted in each.

Two synthetic beds and five research projects · August 2026 · a measurement, not a benchmark

  • 94.6%of the sealed ceiling, second bed
  • +0.2231average precision the planted trap was worth
  • 18 of 18routes that walked through the permission rules
  • 1 of 2predictions written in advance, falsified

An agent, here, means an AI tool given a task and allowed to act on its own for a while — reading files, writing code, running commands — rather than answering one question at a time. The architecture this page measures is described on how I work.

The finding

What caught things was independent reading

Start with the part the design got wrong. This pattern leans on enforcement: written contracts an agent has to satisfy, gates it has to pass, checks that run whether or not anyone asks for them. That machinery is what I would have defended if challenged, and across the two synthetic beds and the five research projects it produced approximately one mechanical refusal.

That count is approximate on purpose, and it is worth being precise about why. It is a tally of refusals I happened to notice, in an unblinded record, with no mechanism anywhere in the pipeline that would have logged a counter-instance. So it is a ratio between things I saw, not a rate over things that occurred, and it should be read as an order of magnitude rather than as a measurement. What it is good enough to support is the comparison: the enforcement machinery was not where the catches came from.

They came from reading. A session belonging to an unrelated project, and an auditor with no stake in the outcome — each handed the situation rather than a checklist. The absence of the checklist is the active ingredient rather than an oversight. A checklist can only carry what somebody already anticipated, and the failures worth catching are the ones nobody did. The enforcement is not worthless: it is cheap, it runs unattended, and it holds a floor. It is simply not what found the problems, and I had the emphasis backwards.

The seal taught the same lesson at a lower layer

Every dataset in the two beds was generated with a known true answer, and that answer was sealed away from the agent behind permission rules — the whole point of the exercise being that a result you can check beats a result you assert. An audit tried 18 routes against those rules, and every one of the 18 succeeded: literal paths, tilde and absolute forms, paths assembled from shell variables at runtime, globs, upward traversal, find, other interpreters — Python, Perl, Node — and a tar of the whole tree. The audit reported reaching the file inventory and stopped there without reading the files.

The mechanism is not subtle once seen. The rules matched on command text, and a shell is not command text: no rule that reads a string can evaluate one, so any construction that defers the path until runtime is invisible to it. What held was an operating-system boundary — a root-owned directory the agent's account could not read at all. The tool layer is where the rule was easy to write. The kernel is where it was true.

Both failures are the same failure. A rule that lists what it expects catches only what was expected of it.

The rig

A dataset where I already knew the answer

Seven projects over roughly four weeks in August 2026: five ordinary research projects, and two synthetic beds built to make the pattern measurable rather than describable.

The distinction between those two groups is the one that matters, and softening it would wreck the piece. The five research projects have no sealed answer. Whatever an agent produced on them can be reviewed, argued with and improved, but it cannot be scored, because there is no key to score it against. Only the two beds had one.

A bed is a synthetic dataset generated for the purpose, so that the true relationship between the columns and the outcome is a thing I chose rather than a thing to be discovered. Into each one I planted a trap: a column that looks powerfully predictive and is inadmissible, because its value does not exist at the moment the prediction is for. This is target leakage — information about the outcome smuggled into the inputs — and it is the single most common way a modelling result is quietly wrong. A model that uses it scores brilliantly and is worth nothing, because at the moment a real decision has to be made the column is blank.

Two things were then sealed away from the agent: the generating process, and the answer key. The consequence is what makes the exercise worth running. The agent could not compute how good a model could possibly be, so it could not grade itself, tune toward the key, or argue with the score afterwards. I could compute all of that from the sealed side, with a script the bed never had access to. The results below are checkable rather than asserted, and that property comes entirely from the boundary rather than from anything the agent did.

The first bed

The first bed reported a number above its own ceiling

The agent used the planted trap and reported a headline of 0.7802. Excluding the trap, the honest score for that same work is 0.1994. The best score any model could have achieved on that bed without the trap — measured afterwards from the sealed side — is 0.367.

So the reported headline was more than twice the ceiling of the problem it claimed to have solved. That is not a subtle statistical error. It is arithmetically impossible on its face: a number above the ceiling is proof that information from after the outcome is in the feature set, and it needs no domain knowledge to spot, only the ceiling. Nothing in the pipeline remarked on it — not the contracts, not the gates, not the automated checks, and not the agent, which had no ceiling to compare against because I had sealed it away.

That last clause is the honest reading, and it cuts both ways. The seal is what makes the measurement trustworthy and it is also what removed the one check that would have caught the error instantly. The revision after this bed was to stop expecting enforcement to notice, and to put an independent reader in the loop instead.

The second bed

The second bed excluded the trap on the reasoning that settles it

The second bed ran on 28 August 2026, on a card-fraud problem: 833,391 transactions from 2024 to train on, 104,160 from the first quarter of 2025 to score against, sharing no customers at all. Fraud is 2.62% of the training file and 2.06% of the scoring file.

Before fitting anything, the agent was required to ask of each of the 34 columns a single question: at what moment does this column's value become known, relative to the moment of the transaction being predicted? Thirty could be placed as knowable then. Two could not be placed at all and were recorded as unresolved rather than waved through. Two could be ruled out — one is the outcome itself, and the other is the trap: the difference between the amount authorised and the amount finally settled. Settlement happens after authorisation. The column is a fact about what came next, so it cannot be an input to a decision made at the transaction.

The agent excluded it on that reasoning, from the column's name alone, before measuring anything. It then measured what the exclusion cost, as three separately fitted models rather than one subtraction: adding the trap back to the best model, and nothing else, raises average precision — a ranking score where higher is better — from 0.3971 to 0.6194, a difference of +0.2231 (95% paired bootstrap interval +0.2071 to +0.2383). Every improvement the search found across its whole budget, added together, is a small fraction of that one number.

What the honest score is worth

The best admissible model scored 0.3971. From the sealed side, the best achievable average precision on those same admissible columns is 0.4198. So the search reached 94.6% of the achievable score — a figure I can state only because I held the key, and one the report itself leaves standing as PENDING because it was sealed from the material needed to compute it.

Two comparisons from the same sealed measurement put that in proportion. An out-of-the-box gradient boosting fit on the identical 27 admissible columns scores 0.2706, so the search bought +0.1265 over simply running the library — the tuning was worth something real. And the absolute bound using every column, traps included, is 0.5560: any score above that line is proof on its face that post-outcome information has reached the feature set. A line of that kind is the cheapest check in this whole programme, and it is the one bed one had no access to.

How far the search got, against the ceiling and against the trap The honest search reached 0.3971 against an achievable ceiling of 0.4198. The same model with the excluded settlement column added back reaches 0.6194, which is not a candidate model and is drawn hatched. random ranking from the data itself a ranker guessing at random scores the fraud rate itself: 0.0206 0.0206 library defaults from the answer key out-of-the-box gradient boosting on the 27 admissible columns: 0.2706 0.2706 this search from the search the best admissible configuration the 25-run search found: 0.3971 0.3971 achievable ceiling from the answer key the best average precision attainable on the admissible columns: 0.4198 0.4198 with the trap from the search the same model with the settlement column added back: 0.6194 — not a candidate model ✕ 0.6194 0 0.2 0.4 0.6 average precision, higher is better · whiskers are 95% intervals ✕ hatched = uses a column excluded on availability grounds

Chart scrolls sideways on a narrow screen — or open “Show the numbers” below.

Show the numbers
quantityaverage precisioninterval lointerval himeasured fromis candidate model
a ranker guessing at random scores the fraud rate itself0.0206fileyes
out-of-the-box gradient boosting on the 27 admissible columns0.2706sealedyes
the best admissible configuration the 25-run search found0.39710.37540.4194agentyes
the best average precision attainable on the admissible columns0.4198sealedyes
the same model with the settlement column added back0.61940.59870.6404agentno
What you're looking at: how far an honest search got, and how much further a compromised one appears to get. The top four bars are all legitimate; the bottom one is the same model with the excluded settlement column handed back to it, and it is drawn hatched because it is not a candidate model. Had the exclusion been a formality, the hatched bar would sit alongside the others instead of half again beyond the ceiling. Two of the five carry no whisker because no interval was published for them: the base rate is a property of the file, and the two sealed-side figures are single measurements from the answer key. n = 104,160 validation rows · data 2026-08-28 · not pre-registered · no untouched holdout · agent-ceiling.csv

Three changes carried the search

The search is the less interesting half of that number, and it is worth seeing whole. Across its budget it made 14 distinct changes. Three produced a gain whose interval excludes zero, three produced a real loss, and the remaining 8 bought nothing that can be told apart from noise — including four consecutive attempts at regularisation, and the constructed ratios the final model leans on hardest.

Every change the search made, sorted by effect Of 14 changes, 8 have an interval that crosses zero and bought nothing that can be told apart from noise. Three are real gains and three are real losses. no change model family: linear → tree model family: linear → boosted tree: +0.1039 — a real gain ▲ +0.1039 learning rate 0.1 → 0.05 learning rate 0.1 → 0.05: +0.0205 — a real gain ▲ +0.0205 merchant id → its own fraud rate merchant id → its own fraud rate: +0.0187 — a real gain ▲ +0.0187 learning rate 0.05 → 0.025 learning rate 0.05 → 0.025: +0.0037 — interval crosses zero ≈ +0.0037 5 derived ratios added 5 derived ratios added: +0.0022 — interval crosses zero ≈ +0.0022 column subsampling to 60% column subsampling to 60%: +0.0008 — interval crosses zero ≈ +0.0008 min rows per leaf 20 → 100 min rows per leaf 20 → 100: +0.0007 — interval crosses zero ≈ +0.0007 leaves per tree 31 → 63 leaves per tree 31 → 63: -0.0003 — interval crosses zero ≈ -0.0003 leaf-value shrinkage 0 → 10 leaf-value shrinkage 0 → 10: -0.0008 — interval crosses zero ≈ -0.0008 leaves per tree 31 → 15 leaves per tree 31 → 15: -0.001 — interval crosses zero ≈ -0.001 encoding fold seed 0 → 7 encoding fold seed 0 → 7: -0.0018 — interval crosses zero ≈ -0.0018 feature set 27 → 15 columns feature set 27 → 15 columns: -0.0056 — a real loss ▼ -0.0056 class re-weighting on class re-weighting on: -0.0073 — a real loss ▼ -0.0073 boosting rounds 100 → 400 boosting rounds 100 → 400: -0.034 — a real loss ▼ -0.034 -0.04 0 +0.04 +0.08 +0.12 change in average precision ▲ gain ▼ loss ≈ hatched, interval crosses zero (8 of 14)

Chart scrolls sideways on a narrow screen — or open “Show the numbers” below.

Show the numbers
changedelta average precisioninterval lointerval hiverdict
model family: linear → boosted tree0.10390.08590.1218gain
learning rate 0.1 → 0.050.02050.01320.0271gain
merchant id → its own fraud rate0.01870.00950.0281gain
learning rate 0.05 → 0.0250.0037-0.00070.0078inside the noise
5 derived ratios added0.0022-0.00440.0084inside the noise
column subsampling to 60%0.0008-0.00410.0061inside the noise
min rows per leaf 20 → 1000.0007-0.00220.0036inside the noise
leaves per tree 31 → 63-0.0003-0.00620.0059inside the noise
leaf-value shrinkage 0 → 10-0.0008-0.00490.0033inside the noise
leaves per tree 31 → 15-0.001-0.00760.0055inside the noise
encoding fold seed 0 → 7-0.0018-0.0060.0022inside the noise
feature set 27 → 15 columns-0.0056-0.0098-0.0009loss

First 12 of 14 rows. Download all 14.

What you're looking at: every change the search made, sorted by how much it moved the score, with the whisker showing how much that movement could have been chance. A bar whose whisker crosses the vertical line is hatched, because the change cannot be told apart from doing nothing. A search that had found a productive axis would show a run of solid bars marching to the right; this one shows three, then a plateau. The single largest bar is the first structural choice — a linear model swapped for a tree ensemble — and nothing after it comes close. n = 14 changes measured on 104,160 validation rows · data 2026-08-28 · not pre-registered · no untouched holdout · agent-search-changes.csv

Read the full technical report for the second bed (HTML, 1.0 MB) — the agent's own account of every run, generated end-to-end from its committed result files with no number typed by hand. It is reproduced as it was written, including the sections where it declines to answer.

The near-miss

One inference short

The second bed carried a second trap, and what happened to it is more interesting than either catching it or missing it would have been.

The column is a per-merchant risk score whose name states no time window — no "as of", no period. The agent investigated it properly. It established that the score is constant within each merchant and identical for the same merchant across both files. It then computed the two quantities that settle the question: the score correlates 0.943 with that merchant's fraud rate over the training period and 0.838 with the same merchant's fraud rate over the scoring period, while the two periods' own rates correlate only 0.620 with each other.

Read those three numbers together and they say something specific. A score fixed before the data begins should track each period about as well as the periods track one another. This one tracks both periods far better than they track each other, which is the signature of a quantity computed across both — that is, of a summary that has seen the outcomes it is being used to predict.

The correlations that carry the lookahead signature The merchant score tracks each period's own fraud rate more closely than the two periods track each other. A score fixed before the data begins could not do that. score × 2024 rate above the reference score vs its merchant's 2024 fraud rate: r = 0.943 r = 0.943 score × 2025 rate above the reference score vs its merchant's 2025 fraud rate: r = 0.838 r = 0.838 2024 × 2025 rate the reference the 2024 rate vs the 2025 rate: r = 0.62 — the reference r = 0.62 0 0.25 0.5 0.75 1 correlation, 0 to 1 dashed line: how well the two periods track each other

Chart scrolls sideways on a narrow screen — or open “Show the numbers” below.

Show the numbers
paircorrelation
score vs its merchant's 2024 fraud rate0.943
score vs its merchant's 2025 fraud rate0.838
the 2024 rate vs the 2025 rate0.62
What you're looking at: the three correlations the agent computed, with the dashed line marking how well the two periods track each other. A score fixed before the data begins has no way to beat that line, because it cannot know either period better than the periods know one another. Both solid bars sit past it. Had the column been innocent, all three bars would have landed at roughly the same length and there would be nothing to conclude. No intervals are drawn because none were published for these three figures. n = 600 merchants · data 2026-08-28 · not pre-registered · computed across both files · agent-near-miss.csv

The agent computed both correlations, wrote them down, recorded the column unresolved, and excluded it from every reported model. What it did not do is take the last step and say what the pattern means. Its own report states the two readings are "consistent with a standing merchant attribute fixed in advance and with a summary computed across both files, and the two files cannot tell them apart" — a conclusion that is defensible on the evidence it allowed itself, and one step short of the evidence it had already produced.

The behaviour was right and the inference was missing. That is a far more useful failure mode to know about than a miss, because a miss looks like inattention and this looks like caution.

The predictions

One prediction held and one was wrong

Both were written down and sealed before the second bed ran. Publishing the one that turned out wrong is what makes the rest of this page worth reading.

P1 · The first trap would be caught — about 85% confidence — HELD

It was caught, and caught on the route I expected: the meaning of the column rather than the behaviour of its values. A quantity that only exists once a transaction has settled cannot be an input to a decision taken at authorisation. The agent reached that from the column's name, before fitting anything, and no measurement was needed or would have changed it.

P2 · The second trap would go unquestioned — about 80% confidence — FALSIFIED

It was questioned. The agent noticed that the name fixes no time window, investigated, computed the diagnostic correlations, recorded the column as unresolved and excluded it from every reported model. The prediction that it would pass unexamined is simply wrong, and by a wide margin — the examination was thorough. What the agent stopped short of was the conclusion, which is a different thing from never looking.

A prediction I got wrong is worth more here than the one I got right, for two reasons. The first is evidential: written predictions are only worth anything if the falsified ones get published, and a page reporting two hits would be indistinguishable from a page where the predictions were written afterwards. The second is that being wrong is what located the near-miss at all. I expected a miss, found an investigation that stopped one inference short, and that gap is the most specific thing this programme learned about where the pattern needs work — an agent that reasons carefully and then declines to conclude will not be caught by any gate that checks whether the work was done.

Limits

Limits of the evidence

This was measured twice. The measurement is the contribution, and it is not a proof that the pattern works.

  • It is a measurement, not a benchmark. The traps were planted by a session I directed, sealed by me, and scored by me. Two beds, one operator, one model family. Nothing here is a controlled comparison against an alternative, and the numbers should not be read as though it were.
  • Only two of the seven projects had sealed answers. The other five are ordinary research work with no key to score against. They inform the refusal count and nothing else on this page.
  • The refusal count is a ratio, not a rate. An unblinded record, with no mechanism that would have logged a counter-instance, cannot produce a denominator. It supports the direction of the comparison and no figure derived from it.
  • The second bed's headline is optimistic by an amount nothing can bound. No untouched holdout was sealed, by design, and the scoring rows were consulted 583 times over the course of the search before the reported number was computed on them. The 94.6% is a ratio between two figures, one of which carries that optimism.
  • Applying a rule is not the same as noticing a problem. The second bed was instructed in advance, and with emphasis, to check every column's availability. It complied, and complying is all it demonstrates. Whether an unprompted session would have found the trap is exactly the question this design cannot answer, and the agent's own report says so three separate times rather than parking it in a footnote.
  • The search found a plateau rather than a winner. Eight of the second bed's configurations are statistically indistinguishable at the top, and of the fourteen distinct changes the search made, three produced a gain whose interval excludes zero. Calling any one of them "the best" overstates what was measured.
The technical version

The second bed, in figures

Second bed. Scores are average precision on the validation file. Intervals are bootstrap over validation rows.
Quantity Figure Where it comes from
Random ranking (the fraud rate itself) 0.0206 The validation file
Linear model on the admissible columns 0.2505 [0.2332, 0.2701] The agent's first run — the floor
Library defaults on the admissible columns 0.2706 Sealed side, measured after the run
Best admissible model the search found 0.3971 [0.3754, 0.4194] The agent's search; ROC AUC 0.9406
Best achievable on the admissible columns 0.4198 Sealed side — the agent's report leaves this PENDING
Absolute bound using every column, traps included 0.5560 Sealed side; a score above it is proof of leakage
Best model with the settlement trap added back 0.6194 [0.5987, 0.6404] The agent's run 16 — not a candidate model
What the settlement trap alone was worth +0.2231 [+0.2071, +0.2383] Paired bootstrap against the best admissible model
What the two unresolved columns were worth +0.0169 [+0.0127, +0.0209] Paired bootstrap, fitted separately

What the headline is an estimate of0.3971 was computed on all 104,160 validation rows, of which 2,149 are fraudulent, from a model fitted on all 833,391 training rows. Those same validation rows had been consulted 583 times by the time it was computed, and the number of boosting rounds in the final configuration was itself chosen by reading those curves. There is no untouched held-out set in that project, by design, so the figure does not estimate performance on data the search has not touched.

The exploration budget

The second bed fixed one thing in advance and nothing else: a budget of 25 runs, with run 26 refused and the project required to stop and report whatever it had. Each run's written justification was appended before the run executed rather than after its score was known, which is what makes the ledger a record of intent rather than of hindsight. All 25 were spent, including one that errored before producing a score and consumed its row anyway. The stopping reason was the budget, not a result.

Nothing was escalated to me across the whole of that session — the inventory, the column declaration, all twenty-five runs and the report — and the project sat blocked for zero minutes. The agent recorded that as a finding rather than leaving it as an absence, so that "he was not needed" can be told apart from "nobody wrote it down".

Synthetic tabular data generated for the purpose; gradient-boosted trees and logistic regression; paired bootstrap intervals over validation rows; answer keys and generating process held behind a root-owned directory rather than a tool-level permission rule.

← The architecture this measures