mindXtrain / docs /mindXtrain_autoresearch_strategy.md
Gregory-L's picture
autoresearch: the keep/reset search, and the UI modules that were never shipped
c27193b verified
|
Raw History Blame Contribute Delete
21.8 kB
# 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.