Skill optimization with live rollouts: feed and knowledge through agentopt
Date: 2026-09-05
Status: spec 3 of 3 from the 2026-09-05 brainstorm; the user chose
"knowledge + feed, more cases, ship only what passes the existing gate" and
asked for ~/agent-loop-optimizer (agentopt) to be the measurement. Executed
autonomously overnight; the numbers in §5 are what was measured, rescored
after review round 1 against agentopt's decline-aware scorer. Result:
measured, shipped nothing (§6).
Depends on: agentopt at 1cf0b28 (its fixtures for the four
ATTEST_TOOLS router servers, --fixture, --skill, --bank,
--results-dir, rescore, run --candidate), evals/skill_trigger_cases.json
(upstream case format plus an agentopt-only expect key on one case), the
two library specs of the same date (the
knowledge skill now names cite.sync and cite.related).
Problem
evals/optimize_skill_triggers.py scores one cheap model call per case and
compares tool names as strings. On 2026-09-03 it improved feed and symbolic
on that proxy and both candidates failed the transfer gate. It cannot see the
failures that matter on the deployed surface: gemma4:e2b clarifying instead
of calling the router, calling runs.ask without the family it was told,
sending "what are my main areas" to Hermes's built-in memory tool, or
receiving a router's options reply and giving up. agentopt runs the real
Hermes against the real MCP server in an isolated home and scores the
transcript with model-free predicates, which is the measurement this repo's
docs/measurement-lessons.md keeps saying to take.
Decisions
- agentopt is the measurement; attestation supplies cases, seeds and the
fixture database. No agentopt code changes from this side; anything it
needs is a message to its session. Runs use
--bankand--results-dirunder this checkout's.venv/agentopt/, one bank per skill, so nothing lands in the optimizer's tree. - The deployed surface is what is scored. Fixtures
attestation-feed/attestation-knowledgeexpose the router pair only, exactly as the Discord gateway serves them since 2026-09-04. A case expecting a tool the collapsed surface hides is a wrong case, not a wrong skill. - Every case names a measured failure or a real reader phrasing. The 15
added cases (9 feed, 6 knowledge;
knowledge-main-areaswas already on main) are the 2026-09-04 Discord refusals, the agentopt 2026-09-05 findings, molecular-AI phrasings a chemistry reader would type, and -- after review round 1 -- one ambiguity case (no_clarify: false) so the options rule can regress in the other direction and be seen. Two are negatives (expect_tool: null) where the right answer is a hand-off to another skill. - Hand edits to the seed were NOT measured, and this spec first said they
were. Three sentences were added to the skills for the three measured
failures (§3) before any optimizer run. agentopt's
seed-rawdiffers fromseedonly in tool-name canonicalization (dotted vs registered names); both banked seeds contain the edits byte for byte (review round 1). The seed-raw/seed gap in §5 is therefore a canonicalization measurement. Measured 2026-09-06 (§5, "hand edit"): the pre-edit text (the Sep 3 install) and this branch's text at k = 5 on the same bank differ by less than the pre-edit text differs from its own repeat, and the main-areas sentence moved its case 0/5 -> 0/5. The edits stay because they name concrete values a reader can check, not because they moved a number. - Acceptance is unchanged, and was not met.
tagging_eval.gate(): not worse on the primary, better on at least two other models, no wider spread. agentopt's acceptance race runs on TRAIN minibatches (k 3..8); its pre-registered dev check (paired LCB > 0 at α = 0.2, k ≥ 5) is a separate step that never ran here (dev k = 3). The transfer models aregemma4:e4bandqwen3.5:9b; the only rollouts on either are seven seed-raw rollouts on gemma4:e4b from a transfer run cancelled at 13:33 (bank-knowledge-transfer). A candidate ships only when both hold; none did.
1. Cases
Added to evals/skill_trigger_cases.json, upstream format, ids
feed-ranked-today-molecular, feed-daily-feed, feed-new-on-mlips,
feed-equivariant-search, feed-rate-dft-noise, feed-add-chemph,
feed-why-catalysis, feed-not-provenance, knowledge-main-areas,
knowledge-connect-forcefields-catalysis, knowledge-between-md-drug,
knowledge-concepts-protein, knowledge-clusters, knowledge-central-hub,
knowledge-not-summarise, plus feed-ambiguous-followup from review
round 1 (31 -> 46 ids). Splits: 6 train / 9 dev, negatives on dev. The
offline validator (evals/run_skill_trigger_eval.py --offline) checks each
router case against attestation.mcp.routing before any model sees it --
that the router does not RAISE, not that it routes right: on the current
router "which topic is the hub" and "list the concepts I have about
proteins" both reach kg.concepts with no prefix, and "find me the papers
behind the 'diffusion-models' concept" too; those are routing.py gaps a
perfect rollout would still score 1.0 on. knowledge-vs-feed-search
expected feed.search until review round 1, a tool the collapsed knowledge
surface does not serve: it scored 0 on the three candidates that ran it (both
seeds and 826e5e38) by construction and pinned one sixth of every dev mean
below. It now expects kg.ask, which is not neutral: the seeds' rollouts on
it called nothing or a browser tool (still 0), while 826e5e38 already
called kg.ask in two of three, so the change raises the candidate's dev
mean, not the seeds'. A 1.0 on it today is a concept list for a "papers
behind" question -- satisfiable, honest, weak. route_kg("what are my main
research areas") declines with options, so the knowledge skill now leads
its example list with "what do I read about most", which routes.
2. The fixture database
ATTEST_DB points at a copy of the live database (9,407 items, real clicks)
taken 2026-09-05 03:22 into .venv/agentopt/seed.db. A copy, not the live
file: agentopt refuses to launch an attestation fixture without ATTEST_DB,
and a rollout that routes to feed.rate writes a click -- and the copy now
holds autocreated me and user personas, because the fixture's Hermes
home names no persona and the model passed a placeholder on the daily-feed
cases, the un-prompted path measurement-lessons already records as 0/9
versus 9/9 with the persona named. The fixture measures a path the deployed
gateway does not take; a persona field in agentopt's fixture is an open
question on its side.
3. Seed edits (before optimizing)
attestation-knowledge: "my main research areas / what do I read about" iskg.ask, never a memory or notes tool; the graph is the reader's record.attestation-feedandattestation-knowledge: a router reply withok: falseandoptionsmeans ask the reader which, or re-ask the router with the question reworded for the option that plainly fits ("today's ranked feed" isfeed.list); never call the router with empty arguments and never say you lack a tool.attestation-feed: a question about experiment arms, sweeps or a draft's numbers isattestation-provenance's; hand off rather than searching the feed for a sweep name.
4. Runs
From this checkout, ATTESTATION_DIR at this worktree so the fixtures
launch this branch's attest-mcp:
agentopt run --skill feed --fixture attestation-feed --k 3 # baseline, both seeds
agentopt run --skill knowledge --fixture attestation-knowledge --k 3
agentopt calibrate --skill feed --fixture attestation-feed --k 3 # proxy trust
agentopt optimize --skill knowledge --fixture attestation-knowledge --reflection-lm ollama_chat/qwen3.5:9b --max-live-rollouts 120
agentopt report / export
Order: knowledge first for optimize because its case set is smallest and
its measured failure (the memory tool) is the clearest; feed's optimize
runs if the night allows. Transfer on gemma4:e4b and qwen3.5:9b with
agentopt run --model <m> against the exported candidate.
5. Measured
Overnight 2026-09-05 on gemma4:e2b through the real Hermes and the real
attest-mcp (this worktree), rescored 2026-09-05 afternoon against agentopt
main after its scorer stopped crediting a relayed router decline as an
answer (review round 1 found feed-ranked-today-molecular scoring 1.0 on
"Which one would you like me to do?", the exact failure the case was
written for). Banks and agentopt's dated reports are under
.venv/agentopt/; CIs are agentopt's at α = 0.2.
| skill | candidate | k | cases | mean (CI) | calls gate | notes |
|---|---|---|---|---|---|---|
| feed | seed-raw e3411564 |
1 | 15 | 0.51 [0.37, 0.66] n=15 | 9/15 | 0.56 before rescore; six positives never called feed.ask (rate-implicit, explain, capability, digest-lately, rate-dft-noise, add-chemph); user='user' on daily-feed |
| feed | seed fd6804ce |
1 | 15 | 0.51 [0.37, 0.64] n=15 | 10/15 | paired vs seed-raw: +0.00 with a ±0.10 interval -- not a number at k = 1; four of fifteen cases move by 0.33 or more between the two |
| feed | calibrate / optimize | -- | -- | -- | -- | did not run |
| knowledge | seed-raw 12e08030 |
3 | 10 | 0.51 [0.41, 0.61] n=30 (dev 0.48 n=18) | 7/10 | main-areas 0/3, central-hub 0/3, vs-feed-search 0/3 (tool hidden, see §1) |
| knowledge | seed 1dbdede5 |
3 (train topped to 8 by races) | 10 | 0.44 [0.37, 0.52] n=50 (dev 0.43 n=18) | 7/10 | paired vs seed-raw: −0.07, LCB −0.18; main-areas 0/8 -- the edit did not move the case it named |
| knowledge | 826e5e38 (kg.tools description 367 -> 3,803 chars) |
3 dev / 8 train | 9 | dev 0.61 n=18; train 0.94 n=24 | 8/9 | accepted by GEPA on a 3-case train minibatch (subsample score 0.5625 -> 2.92; every paired race in the log was undecided); dev paired vs seed +0.185, LCB +0.084 at α = 0.2, but k = 3 < 5; its text carries "26/26" and "0% of the time" -- the seed's own kg.tools line (1 in 26 vs 26 in 26) garbled into a correctness claim; no second model |
| knowledge | 9dbc7d8c (kg.ask + kg.tools descriptions) |
8 | 3 train | 0.89 n=24 | 3/3 | never scored on dev; race vs 826e undecided |
| both | transfer gemma4:e4b, qwen3.5:9b | 3 | -- | -- | -- | cancelled at 13:33 after seven seed-raw rollouts on gemma4:e4b (bank-knowledge-transfer: connect-topics 3/3, main-areas 0/3); nothing on the candidate, nothing on qwen3.5:9b; the candidate cannot ship (below) |
"calls gate" is cases with at least one rollout passing the gate predicate
(calls_any / calls_none); strict all-rollouts is 5/10, 3/10 and 6/9.
Budget: 150 live rollouts against --max-live-rollouts 120 (agentopt
overshoots to finish a race); knowledge optimize 04:32-07:15, feed baseline
07:15-07:38.
The hand edit, measured (2026-09-06)
agentopt's noise-floor step (its spec §11) on the knowledge skill, run by its
session: three texts of attestation-knowledge/SKILL.md at k = 5 over the
same 10 cases in one bank (knowledge-proof2), this worktree's attest-mcp
(schema 8) on a seeded copy of the database, model unloaded before each arm,
fixture health 0 errored calls and 0 rollouts lost before the agent answered
(a first attempt served the copy from main's schema-6 server, failed inside
every graph call, and scored 0.67 on the partial predicates -- retracted,
quarantined, and the reason every agentopt report now carries a
fixture-health line). Texts are named by sha256 prefix:
| text | sha256 | what it is |
|---|---|---|
| installed | 63460cea |
the Sep 3 attest install copy: pre-edit |
| main | aab7da3d |
adds the "dotted names are for you to read" paragraph |
| worktree | a5a16e0c |
adds the References rewrite (library specs) and the two §3 sentences |
| pair | paired mean | LCB | UCB | verdict |
|---|---|---|---|---|
| installed vs its own repeat (the noise floor) | +0.027 | −0.041 | +0.094 | spread > 0.3 on 1/10 cases (not-todays-feed, exactly 0.30) |
| worktree − installed ("everything since Sep 3") | −0.005 | −0.067 | +0.057 | undecided; smaller than the floor |
| worktree − installed's repeat | +0.022 | −0.046 | +0.090 | undecided |
| main − installed (dotted-names paragraph alone) | -- | -- | -- | not run: the main arm was stopped at the user's call after 34 of 100 rollouts, because the line above is already inside the noise band and splitting it further was not worth the GPU time; the 34 rollouts stay in the bank |
| worktree − main (References rewrite + the §3 sentences) | -- | -- | -- | not run, same reason |
Per case, worktree / installed at n = 5 / 10: connect-topics 0.40 / 0.33,
main-areas 0.00 / 0.00, vs-feed-search 0.00 / 0.00, not-todays-feed
0.70 / 1.00, connect-forcefields-catalysis 0.80 / 0.75, between-md-drug
0.73 / 0.67, concepts-protein 0.87 / 0.80, clusters 0.80 / 0.80, central-hub
0.00 / 0.00, not-summarise 1.00 / 1.00 (calls_none alone; a
mentions_any check follows in the next rescore). The row the §3 edit
exists for, main-areas, is 0/5 under the edited text: the sentence did not
move gemma4:e2b off the built-in memory tool at all. Two cases are
deterministic zeros on every text (main-areas, central-hub) -- failures to
fix in the router or the description, not noise to optimize through.
Feed, two proposers, two students (2026-09-08)
Run by the agentopt session after its scorer, fixture and proposer fixes
(a relayed decline is not an answer; the fixture names a persona; a
proposal carrying a number absent from the reflective data scores 0 and
spends no rollouts): the installed feed SKILL.md as seed, the
attestation-feed fixture with the demo-reader persona, this branch's
16 feed cases (8 train / 8 dev) on the schema-8 seeded copy, qwen3.5:4b
as the reflection model for both proposers, every proposal raced against
the seed over all 8 train cases at 5+5 rollouts (paired LCB, α = 0.2),
then scored on dev. Reports: agentopt results/2026-09-08-feed-gepa-vs-
skillopt.md and results/2026-09-08-feed-qwen9b-train-wins-dev-losses.md.
| student | seed dev (n=40) | proposal | size | train vs seed | dev vs seed |
|---|---|---|---|---|---|
| gemma4:e2b | 0.39 [0.30, 0.47] | GEPA trigger rewrite | 264 -> 2,665 chars | −0.167 [−0.322, −0.011], rejected | -- |
| gemma4:e2b | GEPA body rewrite (shorter by 45%) | 11,678 -> 6,472 | +0.042 [−0.005, +0.089], undecided | -- (digest-lately 0.60 -> 0.87, nothing else) | |
| gemma4:e2b | GEPA examples | 0 -> 2,444 | −0.015, undecided | -- | |
| gemma4:e2b | SkillOpt examples edit | 0 -> 808 | +0.015 [−0.031, +0.061], undecided | -- (explain 0.00 -> 0.13, search-topic 1.00 -> 0.87) | |
| qwen3.5:9b | 0.41 [0.33, 0.49] | GEPA trigger rewrite | 264 -> 3,335 | +0.26, LCB +0.17, accepted | 0.00 [−0.07, +0.07] |
| qwen3.5:9b | GEPA body rewrite | 11,678 -> 5,714 | +0.13, LCB +0.03, accepted | −0.09, UCB −0.01, worse | |
| qwen3.5:9b | SkillOpt trigger, three appended rules | 264 -> 1,460 | +0.22, LCB +0.13, accepted | −0.13 [−0.27, +0.01]; not-provenance 0.80 -> 0.20 |
Fixture health clean on both (0 errored fixture calls); on the 9B, 39 of
358 rollouts timed out at 300 s and score 0, unevenly across candidates,
and excluding them changes no verdict. The persona fixture fixed the
placeholder-user failure (persona-name 0.80 with user="demo-reader" on
every call, against 0/9 unprompted in this repo's own measurement). Five
of sixteen cases are 0.00 on every gemma rollout (capability,
rate-implicit, ambiguous-followup, explain, rate-dft-noise); two of them
(capability, rate-implicit) moved for no candidate on either student.
What the two students say together: on gemma4:e2b nothing is decided in either direction except that a tenfold-longer trigger is worse, and the only near-win shortened the body; on qwen3.5:9b every accepted candidate wins the eight train questions with a closed interval and is flat or worse on the eight it never saw, and the one whose text can be read ("feed content queries always require tool use") breaks the hand-off case by construction. That is overfitting to eight questions, not a lack of search power, and it is the train/dev gap the pre-registered dev bar exists to catch. The pre-registered bar (dev k ≥ 5, paired LCB > 0) is unmet by every candidate on both students.
Caveat on every number above (found 2026-09-09). All of these rollouts
ran Hermes with its full local toolset -- terminal, file read/write, code
execution, outbound curl -- and the fixture's cwd was the checkout's
examples/workspace. On the qwen3.5:9b feed run that was 215 terminal,
162 search, 87 read, 12 write and 9 execute calls plus 16 curls over 358
rollouts, with writes into this worktree (a staged edit to
feed_candidates.toml) and nine files in the user's home. The scores are
what they are, but "called feed.ask" was measured on an agent that could
also grep the source and curl arXiv, which the deployed Discord gateway
cannot. agentopt now disables every toolset except skills and clarify,
copies the workspace fresh per rollout, and reports WORKSPACE WRITES on
the fixture-health line; anything cited from these banks as a decision
input should be rerun under that guard.
6. Result: measured, shipped nothing
- The one accepted candidate changes only
kg.tools' tool description -- ten times longer, carrying numbers misattributed from the seed's own measurement line into a correctness claim and a "Failure Prevention Checklist" -- and its exportedSKILL.mdcarries the seed's body and trigger unchanged (the export reshapes frontmatter, an agentopt artefact, not a candidate edit). The gain, real or not, lives in a string the MCP server serves, not in the skill; a candidate whose gain is in a tool description is telling us the shipped description is the weak part, and the fix belongs inmcp/knowledge.py's docstring under its own measurement, not inSKILL.md. Invented numbers in a description loaded into every Hermes turn are disqualifying on their own; agentopt has logged the proposer-side rule (no numbers absent from the reflective dataset) on its side. - What the scorer cannot see, listed so the next run does not trust it:
a negative case (
feed-not-provenance,knowledge-not-summarise) scores 1.0 oncalls_nonealone, so a correct hand-off and the pseudo-callsession_search(query="kdsweep sweep WER")printed as prose score the same (rollout ids relayed to agentopt; amentions_anypredicate or a judged one is an open question there); the fixture names no persona (§2); the offline validator checks that the router does not raise, not that it routes right (§1). - What changed on this branch because of the measurement: the
unsatisfiable dev case now expects a tool the surface serves; the
main-areas sentence in the knowledge skill is the short form that names
the values ("what do I read about most" is
kg.ask; "I do not have access to your reading" is never true withkg.askin the tool list) rather than a narrative about a memory tool the transcripts do not show; andfeed-ambiguous-followupexists so the options rule ("when one option plainly fits, re-ask the router") has a case that fails when the model re-asks instead of asking. - The honest one sentence, after the overnight runs (2026-09-05). On
gemma4:e2b through the real path, the hand edits were not measured; the
feed baseline is 0.51 either way with a ±0.10 paired interval and no
calibration; the knowledge seed scored 0.44 against 0.51 for the same
text under dotted names (not significant at α = 0.2); and GEPA's single
accepted candidate is +0.18 on six dev cases at k = 3 (paired LCB +0.08
at α = 0.2 -- a positive signal, blocked by the pre-registered k ≥ 5,
with one of the six unsatisfiable), on one model, with a description
that misstates the seed's numbers -- so
tagging_eval.gate()cannot be evaluated from anything on disk and nothing meets the bar, which is not the same as nothing being there. - The honest one sentence, after the proof runs (2026-09-06 to 08). With the scorer, the fixture and the proposer fixed and the model unloaded between arms: the knowledge hand edit sits inside the noise band and left its target case at 0/5; on feed, gemma4:e2b decides nothing except that a tenfold-longer trigger is worse, and qwen3.5:9b accepts three candidates on the train questions that are flat or worse on the held-out ones -- so what the skill text can move on these students, at this case count, is smaller than the noise floor or does not transfer, and the cases that stay at zero on every text (capability, rate-implicit, main-areas, central-hub) need a router phrase, a description or a persona line, not a skill rewrite. Nothing ships; the optimizer, the harness and the fixture-health line are what this spec produced.
What this spec does not decide
Whether kg.ask gets a route to cite.related; the four route_kg gaps
§1 names (hub -> kg.central, "concepts about X" -> kg.concepts(prefix),
"papers behind a concept" -> feed.search or a kg.ask answer that names
it, "my main research areas" -> kg.central instead of a decline);
symbolic's expression parser (a router gap agentopt found:
integral(x^2, x, 0, 1) and sin^2(x) fail to parse); provenance's
family argument, which is a case that must keep failing until the
provenance skill fixes it; a measured rewrite of kg.tools' description
(the finding in §6); a pre-edit run that would measure the §3 sentences;
and whether the harness should name a persona (agentopt's call).