File size: 21,836 Bytes
c27193b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 | # mindXtrain Autoresearch Strategy
**Subject:** fold the four transferable disciplines from `gke-labs/open-rl/examples/autoresearch` into mindXtrain
**Author target:** Professor Codephreak
**Convention:** cypherpunk2048 β Apache-2.0, no admin keys, no proxies, mainnet-only, Foundry canonical, Python β₯3.12, flat snake_case, least code
**Status:** plan of record
---
## 0. Thesis
mindXtrain already *is* an autoresearch system. `governance/proof_loop.py` runs compose β derive params β train β probe before/after β classroom β boardroom β record feedback β suggest next params. `autotune/feedback.py` is the append-only ledger + hill-climb. `eval/mei/history.py` is the strictly-higher promotion gate. What `proof_loop` is **not** is an *iterated search with verifiable per-attempt provenance*: it runs once, nudges scalars in-process, and never makes the attempt itself a first-class, committed, re-checkable object.
open-rl/autoresearch is a thin Karpathy-style demo and is **behind** mindXtrain on substance β but it nails exactly the four disciplines mindXtrain lacks. This strategy lifts the *disciplines*, not the code, and wires them onto primitives that already exist (`proof_loop`, `feedback`, `mei/history`, `receipt_emit`, `manifest`, `MindXTrainRegistry`, `runs.py`).
Net new surface: one package (`mindxtrain/research/`), one Solidity sibling contract, one CLI verb, one operator route, one k8s overlay. Everything else is a wrapper over code that already passes its suite.
---
## 1. The four disciplines being imported
| # | Discipline (open-rl) | mindXtrain gap | Lands on |
| - | --- | --- | --- |
| 1 | Attempt = committed diff to one declared editable file; dedup by commit; `git reset --hard` discards a loser | proof_loop nudges scalars in-process; the attempt is not a git object, not re-checkable, not anchorable | `research/attempt.py` + `receipt_emit`/`manifest`/`MindXTrainRegistry` |
| 2 | Declarative harness contract (`command`/`editable`/`metric`/`metric_mode`) decoupling the keep-reset loop from the benchmark | proof_loop is hard-wired to the persona-imprint recipe + classroom delta | `research/contract.py` (pydantic) |
| 3 | Editable allowlist: harness aborts if the working tree touches anything outside the declared file | `approval.py` + doom-loop guard exist, but no git-scoped write fence | `research/guard.py` |
| 4 | Durable append-only `ui_events.jsonl` + read-only observer | `runs.py` is a 200-event in-memory ring buffer; history dies on restart | `research/ledger.py` feeding `operator/coach` |
Explicitly **not** imported: the Gemini-CLI agent harness (mindXtrain has `operator/agent_loop.py` + vllm/ollama/openai_compat backends), the Tinker SDK / `tinker_cookbook` abstraction (committed to trl/torchtune/unsloth/primus), their UI, their k8s base. Filed-for-later: open-rl is a self-hosted Tinker-compatible gateway, so it is a known interop target if tinker-cookbook recipes are ever wanted β not now.
---
## 2. Architecture: the research harness as a wrapper
```
mindxtrain/research/
__init__.py
contract.py # AttemptContract pydantic model (discipline 2)
guard.py # git editable-allowlist fence + dedup (disciplines 1, 3)
attempt.py # run one measured attempt β AttemptResult (discipline 1)
ledger.py # append-only attempts.jsonl + read API (discipline 4)
search.py # the outer keep/reset loop over N attempts
anchor.py # champion β manifest β MindXTrainRegistry / sibling contract
contracts/
mei_imprint.toml # optimize classroom imprint_delta on the CPU lane
mei_checkpoint.toml # optimize MEI on a real checkpoint
bfcl.toml # optimize tool-call accuracy
```
The harness owns: git discipline, the metric source of truth, keep/reset, the durable ledger, and anchoring. The *recipe* owns: how to run one attempt and what number to emit. This is the open-rl separation, expressed in mindXtrain types.
### 2.1 The attempt contract (discipline 2)
A recipe-agnostic contract. The metric is named, and its source is one of mindXtrain's existing evals β not a bespoke loop per benchmark.
```python
# mindxtrain/research/contract.py
from __future__ import annotations
from pathlib import Path
from typing import Literal
from pydantic import BaseModel, ConfigDict, Field
MetricSource = Literal["mei", "classroom_delta", "bfcl", "tau_bench", "held_out_loss", "metrics_jsonl"]
class AttemptContract(BaseModel):
"""Declarative contract for one autoresearch attempt.
`command` runs one measured attempt and must exit nonzero on failure.
`editable` is the *only* path the agent may change; the guard enforces it.
`metric` names the scalar to optimize; `source` says where to read it.
"""
model_config = ConfigDict(extra="forbid", frozen=True)
task: str
command: str # {run_dir} {run_name} {attempt_timeout_minutes} substituted
editable: list[Path] = Field(min_length=1)
metric: str # e.g. "imprint_delta", "mei", "accuracy"
source: MetricSource = "metrics_jsonl"
mode: Literal["max", "min"] = "max"
attempt_timeout_minutes: float = 10.0
# keep iff strictly better by this margin (mirrors the Β§8 MEI gate's strict-higher rule)
min_improvement: float = 0.0
```
`source` is the bridge to existing code. Resolution table (all already in-tree):
- `mei` β `eval/mei/score.py` β `MEIScore`, compared via `eval/mei/history.py` (the Β§8 strictly-higher gate β this *is* the keep criterion, free)
- `classroom_delta` β `governance/classroom.py::ClassroomReport.imprint_delta`
- `bfcl` β `eval/bfcl.py`
- `tau_bench` β `eval/tau_bench.py`
- `held_out_loss` β `eval/held_out_loss.py` (mode=`min`)
- `metrics_jsonl` β read `run_dir/metrics.jsonl` last row (the open-rl convention, for arbitrary recipes)
### 2.2 The git guard (disciplines 1 + 3)
Ported wholesale in spirit from open-rl `run_attempt.py`, minus the Gemini specifics. Three rules:
1. **Commit-before-measure.** Refuse to run if the declared editable has uncommitted changes. Forces every attempt to be a real commit β re-checkable, anchorable.
2. **Editable fence.** Refuse if `git status --porcelain` shows changes *outside* the editable allowlist. The agent may touch one file; nothing else moves.
3. **Dedup-by-commit.** Refuse to re-measure a commit already scored for this researcher (lookup against the ledger). Keeps the search honest and the anchor set unique.
```python
# mindxtrain/research/guard.py (sketch)
def assert_clean_and_committed(editable: list[Path], root: Path) -> str:
paths = [str(p) for p in editable]
# rule 2: nothing dirty outside the allowlist
dirty = _git("status", "--porcelain", cwd=root).splitlines()
stray = [ln for ln in dirty if ln[3:].strip() not in paths]
if stray:
raise ResearchAbort(f"working tree dirty outside editable: {stray}")
# rule 1: editable itself must be committed (no staged/unstaged edits left)
if _git("status", "--porcelain", "--", *paths, cwd=root).strip():
raise ResearchAbort("commit the editable file before measuring")
return _git("rev-parse", "--short=12", "HEAD", cwd=root).strip()
def assert_new_commit(commit: str, researcher: str, ledger: Ledger) -> None:
if ledger.has_commit(researcher, commit):
raise ResearchAbort(f"commit {commit} already scored for {researcher}")
```
This is the single most valuable import: it converts the search trail into a sequence of signed-off git commits, which is precisely what `receipt_emit.py` already wants (`_git_sha()` is already read into the manifest).
### 2.3 The attempt runner (discipline 1)
Wraps the existing run path. For the CPU imprint contract it calls `proof_loop.run_proof_loop`; for a checkpoint contract it shells the `command` and reads the metric source. Bounded wall-clock (identical per attempt so scores are comparable β open-rl's `ATTEMPT_TIMEOUT_MINUTES` invariant), SIGTERMβSIGKILL process-group teardown, partial-metric write on timeout (exit 124).
```python
# mindxtrain/research/attempt.py (control flow)
def run_attempt(contract, researcher, run_dir, ledger) -> AttemptResult:
root = repo_root()
commit = assert_clean_and_committed(contract.editable, root) # disciplines 1,3
assert_new_commit(commit, researcher, ledger) # dedup
ledger.append(kind="attempt", researcher=researcher, commit=commit,
status="running", run_dir=run_dir) # discipline 4
code = run_logged(format_cmd(contract.command, run_dir), run_dir/"logs.log",
timeout_s=contract.attempt_timeout_minutes*60)
value = read_metric(contract, run_dir) # discipline 2 resolution
status = "completed" if code == 0 and value is not None else \
"timed_out" if code == 124 else "failed"
capture_diff(contract.editable, run_dir, root) # code.diff for the UI
ledger.append(kind="attempt", researcher=researcher, commit=commit,
status=status, metric={"name": contract.metric, "value": value,
"mode": contract.mode})
return AttemptResult(commit=commit, value=value, status=status, run_dir=run_dir)
```
### 2.4 The search loop (keep/reset)
The outer loop. Keep criterion reuses the Β§8 strictly-higher gate semantics directly: an attempt is kept iff it beats the current champion by `min_improvement` in the declared direction. Losers are `git reset --hard` to the champion commit. This is `feedback.suggest_next_params` driving the edit when the editable is `config.toml`/recipe YAML, or the operator's own `agent_loop` driving the edit when the editable is code.
```python
# mindxtrain/research/search.py (control flow)
def search(contract, researcher, log_root, max_attempts, propose_edit):
ledger = Ledger(log_root)
champion = run_baseline(contract, researcher, log_root, ledger) # record default-config row first
for i in range(max_attempts):
propose_edit(contract.editable, champion, ledger) # feedback nudge OR agent_loop
commit_editable(contract.editable, msg=f"attempt {i+1}")
result = run_attempt(contract, researcher, new_run_dir(log_root, researcher, i+1), ledger)
if improved(result.value, champion.value, contract.mode, contract.min_improvement):
champion = result
anchor_champion(champion, contract) # Β§3: manifest + on-chain
else:
git("reset", "--hard", champion.commit) # discard loser
return champion
```
`propose_edit` is the seam where the two existing mindXtrain edit drivers plug in:
- **scalar/recipe edits** β `autotune.feedback.suggest_next_params` (already the proof-loop nudge)
- **code edits** β `operator.agent_loop.run_agent_loop` with the doom-loop detector (already built), fenced to one file by the guard
---
## 3. Provenance: every champion is verifiable and anchored
This is where the import compounds with the ecosystem and earns its keep. The champion of each search is already a git commit; bind it through the existing receipt path to chain.
### 3.1 Off-chain (exists today, just call it)
`provenance/manifest.py::emit_receipt_for_run` already hashes config + checkpoint + AutotunePlan with BLAKE3 and reads `git_sha`. `operator/receipt_emit.py::emit_run_receipt` already writes `manifest.json` into the run dir. The only new line is: on champion promotion, call `emit_run_receipt` with the attempt's commit as `git_sha`, and include the attempt's `code.diff` BLAKE3 in the manifest so the *edit itself* is hashed, not just the result.
Add one field to `ProvenanceHashes`:
```python
attempt_diff: str = Field(default="", description="BLAKE3 of the kept attempt's code.diff")
```
### 3.2 On-chain (existing contract handles per-run; add a sibling for the search tree)
`MindXTrainRegistry.anchor()` is write-once per `runId` and already deployed-shaped for champion receipts β anchor each search's final champion there, no change. But the *value* of autoresearch is the lineage (which edits helped), and the existing contract can't express a parentβchild attempt tree. Per cypherpunk2048 (no modifying deployed contracts), add an immutable **sibling**:
```solidity
// contracts/src/mindx_autoresearch_registry.sol
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.26;
/// @title MindXAutoresearchRegistry
/// @notice Write-once anchoring of an autoresearch attempt lineage.
/// @dev cypherpunk2048: immutable, no proxy, no Ownable, no admin keys, no setter.
contract MindXAutoresearchRegistry {
struct Attempt {
bytes32 parentCommit; // bytes32(0) for the baseline
bytes32 diffHash; // BLAKE3 of code.diff
bytes32 metricHash; // keccak(metric_name, mode, value_18dp)
bool kept; // did it beat the champion
address researcher;
uint64 timestamp;
}
mapping(bytes32 => Attempt) private _attempts; // key: attempt commit hash
event AttemptAnchored(
bytes32 indexed commit, bytes32 indexed parentCommit, address indexed researcher,
bytes32 diffHash, bytes32 metricHash, bool kept, uint64 timestamp
);
error AttemptExists(bytes32 commit);
function anchor(
bytes32 commit, bytes32 parentCommit, bytes32 diffHash, bytes32 metricHash, bool kept
) external {
if (_attempts[commit].timestamp != 0) revert AttemptExists(commit);
Attempt memory a = Attempt(parentCommit, diffHash, metricHash, kept, msg.sender, uint64(block.timestamp));
_attempts[commit] = a;
emit AttemptAnchored(commit, parentCommit, msg.sender, diffHash, metricHash, kept, a.timestamp);
}
function get(bytes32 commit) external view returns (Attempt memory) { return _attempts[commit]; }
function exists(bytes32 commit) external view returns (bool) { return _attempts[commit].timestamp != 0; }
}
```
Anchoring policy: anchor **kept** attempts always (the champion lineage), and optionally anchor losers in batch for a fully reproducible search log. Metric encoded as `keccak(abi.encode(name, mode, uint256 value_18dp))` to match the uint256 scientific-precision convention used across the stack.
### 3.3 Settlement (existing x402, optional gate)
`provenance/x402.py::issue_invoice` already targets the bankon facilitator and USDC ASA 203977300. If autoresearch runs are sold as a service (a researcher pod billed per search), gate `anchor()` behind an x402 settlement check via the existing `X402Receiver` contract β `Deploy.s.sol` already wires `("algorand", 203977300)` as the asset-id hash. No new payment code; reuse the gate.
---
## 4. Foundry + mainnet (per standing convention)
Foundry is canonical for tests; Base mainnet is the deploy target (matches `manifest.INFTPointer` chain=`base` and `foundry.toml` rpc endpoints).
### 4.1 Test (`contracts/test/MindxAutoresearchRegistry.t.sol`)
```solidity
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {MindXAutoresearchRegistry} from "../src/mindx_autoresearch_registry.sol";
contract MindXAutoresearchRegistryTest is Test {
MindXAutoresearchRegistry reg;
function setUp() public { reg = new MindXAutoresearchRegistry(); }
function test_AnchorThenGet() public {
reg.anchor(bytes32("c1"), bytes32(0), bytes32("d1"), bytes32("m1"), true);
assertTrue(reg.exists(bytes32("c1")));
assertEq(reg.get(bytes32("c1")).parentCommit, bytes32(0));
assertTrue(reg.get(bytes32("c1")).kept);
}
function test_WriteOnce() public {
reg.anchor(bytes32("c1"), bytes32(0), bytes32("d1"), bytes32("m1"), true);
vm.expectRevert(abi.encodeWithSelector(MindXAutoresearchRegistry.AttemptExists.selector, bytes32("c1")));
reg.anchor(bytes32("c1"), bytes32(0), bytes32("d2"), bytes32("m2"), false);
}
function testFuzz_LineageChains(bytes32 parent, bytes32 child) public {
vm.assume(parent != child && child != bytes32(0));
reg.anchor(child, parent, bytes32("d"), bytes32("m"), true);
assertEq(reg.get(child).parentCommit, parent);
}
}
```
Run: `forge test -vvv` (fuzz runs=256 per existing `foundry.toml`).
### 4.2 Deploy (extend the existing `Deploy.s.sol`)
Add one line to the existing broadcast block; do not touch the deployed `MindXTrainRegistry`/`X402Receiver`:
```solidity
MindXAutoresearchRegistry research = new MindXAutoresearchRegistry();
console.log("MindXAutoresearchRegistry deployed at:", address(research));
```
Deploy: `forge script script/Deploy.s.sol --rpc-url base --broadcast --verify`. Record the address into `mindxtrain/research/anchor.py` as a module constant (no admin key, nothing to configure post-deploy).
---
## 5. Operator + CLI + parallel researchers
### 5.1 CLI verb (Typer, matches `mei_score`/`imprint` house style)
```python
# mindxtrain/cli/main.py
@app.command()
def research(
contract: Path = typer.Argument(..., help="Path to the AttemptContract TOML."),
researcher: str = typer.Option("codephreak", "--researcher", help="Researcher id."),
max_attempts: int = typer.Option(10, "--max-attempts", "-n"),
log_root: Path = typer.Option(Path("./out/research"), "--log-root"),
anchor: bool = typer.Option(False, "--anchor", help="Anchor kept champions to Base mainnet."),
) -> None:
"""Run an autoresearch search: iterate edits on one file, keep iff the metric strictly improves."""
from mindxtrain.research.search import search_from_contract
result = search_from_contract(contract, researcher, max_attempts, log_root, do_anchor=anchor)
console.print(result.summary())
```
### 5.2 Operator route + durable ledger (discipline 4)
`runs.py`'s in-memory ring buffer stays for live SSE, but `research/ledger.py` writes an append-only `attempts.jsonl` per researcher that survives restarts. Add a coach route `GET /coach/api/research/{researcher}/attempts` that reads the ledger (the open-rl read-only-observer pattern). The existing coach UI (`operator/coach/static/`) gets one new tab rendering the attempt table + the per-attempt `code.diff`, fed by the ledger rather than memory. The SSE stream (`runs.py`) and the durable ledger are complementary: live view from memory, history + provenance from disk.
### 5.3 Parallel researchers (k8s)
Generalize `ops/k8s/train_job.yaml` into a researcher overlay: N pods, each a clean clone, each a distinct `--researcher` id, all hill-climbing the same metric against the shared mindX gateway (`mindx.pythai.net`), all writing to one shared `log_root` so the ledger aggregates every researcher's attempts. Gate pod start on a `READY_URLS`-style health check of the gateway/vllm (open-rl's race-avoidance). Caveat: the MI300X lanes are GPU-bound, so parallelism is bounded by GPU slices β the CPU `mei_imprint.toml` contract (the `trl_cpu` lane) is the one that parallelizes freely and is the right default for wide search.
---
## 6. Phased rollout
| Phase | Deliverable | Gate (test target) |
| - | --- | --- |
| P1 | `research/contract.py` + `research/guard.py` + `research/ledger.py` | `pytest tests/test_research_guard.py tests/test_research_ledger.py` β commit fence, editable allowlist, dedup, durable append |
| P2 | `research/attempt.py` + `research/search.py` wrapping `proof_loop` on the CPU imprint contract | `pytest tests/test_research_search.py` β baseline row, keep/reset, `git reset --hard` on loser, comparable timeouts |
| P3 | `mindx_autoresearch_registry.sol` + Foundry test + extend `Deploy.s.sol` | `forge test -vvv` green (incl. fuzz), then `forge script ... --rpc-url base --broadcast --verify` |
| P4 | `research/anchor.py` binding champion β `emit_run_receipt` β sibling contract; add `attempt_diff` hash to manifest | `pytest tests/test_research_anchor.py` + on-chain read-back of a testnet anchor |
| P5 | CLI `research` verb + coach route + UI attempt tab | `pytest tests/test_coach_research_api.py` + manual coach UI check |
| P6 | k8s researcher overlay + `READY_URLS` gate; wide CPU search | live N-researcher run against `mindx.pythai.net`, ledger aggregates all |
Each phase is independently shippable and keeps the suite green (ruff + mypy clean per house rule). P1βP2 deliver the core discipline with zero new dependencies (stdlib + pydantic, base-install importable, same as `feedback.py`/`mei/history.py`). P3βP4 deliver verifiability. P5βP6 deliver scale.
---
## 7. What this buys you
- **Every hyperparameter/edit decision becomes a signed-off git commit** with a BLAKE3-hashed diff, re-checkable and dedup-protected β the search trail is now an artifact, not a vibe.
- **The keep criterion is your existing Β§8 MEI strictly-higher gate**, generalized to any eval (classroom delta, BFCL, tau_bench, held-out loss) via one declarative contract β no more one-loop-per-benchmark.
- **Champion lineage is anchored immutably on Base mainnet** through the receipt path you already built, with an optional x402/Algorand settlement gate through the facilitator you already run β autoresearch provenance native to the BANKON stack, which open-rl has no concept of.
- **Wide parallel search** across researcher pods sharing the mindX gateway, with a durable ledger that survives restarts β the one operational pattern open-rl genuinely does better than the current `runs.py` ring buffer.
The import is ~1 package, 1 contract, 1 CLI verb, 1 route, 1 overlay β wrapping primitives that already pass their suite. Nothing in `proof_loop`, `feedback`, `mei`, `manifest`, or the deployed registry has to change; it only gets a verifiable iteration shell around it.
|