fieldkit 0.2.0__tar.gz → 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. {fieldkit-0.2.0 → fieldkit-0.3.0}/CHANGELOG.md +34 -0
  2. {fieldkit-0.2.0 → fieldkit-0.3.0}/PKG-INFO +18 -7
  3. fieldkit-0.3.0/README.md +66 -0
  4. {fieldkit-0.2.0 → fieldkit-0.3.0}/docs/api/cli.md +4 -4
  5. fieldkit-0.3.0/docs/api/eval.md +224 -0
  6. fieldkit-0.3.0/docs/api/lineage.md +118 -0
  7. fieldkit-0.3.0/docs/api/training.md +85 -0
  8. fieldkit-0.3.0/samples/hello-lineage.py +164 -0
  9. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/_version.py +1 -1
  10. fieldkit-0.3.0/src/fieldkit/lineage/__init__.py +532 -0
  11. fieldkit-0.3.0/tests/test_lineage.py +408 -0
  12. fieldkit-0.2.0/README.md +0 -55
  13. fieldkit-0.2.0/docs/api/eval.md +0 -91
  14. {fieldkit-0.2.0 → fieldkit-0.3.0}/.gitignore +0 -0
  15. {fieldkit-0.2.0 → fieldkit-0.3.0}/LICENSE +0 -0
  16. {fieldkit-0.2.0 → fieldkit-0.3.0}/docs/api/capabilities.md +0 -0
  17. {fieldkit-0.2.0 → fieldkit-0.3.0}/docs/api/nim.md +0 -0
  18. {fieldkit-0.2.0 → fieldkit-0.3.0}/docs/api/rag.md +0 -0
  19. {fieldkit-0.2.0 → fieldkit-0.3.0}/pyproject.toml +0 -0
  20. {fieldkit-0.2.0 → fieldkit-0.3.0}/samples/bench-rag.py +0 -0
  21. {fieldkit-0.2.0 → fieldkit-0.3.0}/samples/feasibility-math.py +0 -0
  22. {fieldkit-0.2.0 → fieldkit-0.3.0}/samples/hello-nim.py +0 -0
  23. {fieldkit-0.2.0 → fieldkit-0.3.0}/samples/naive-rag.py +0 -0
  24. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/__init__.py +0 -0
  25. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/capabilities/__init__.py +0 -0
  26. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/capabilities/data/__init__.py +0 -0
  27. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/capabilities/data/spark-capabilities.json +0 -0
  28. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/cli/__init__.py +0 -0
  29. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/eval/__init__.py +0 -0
  30. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/nim/__init__.py +0 -0
  31. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/rag/__init__.py +0 -0
  32. {fieldkit-0.2.0 → fieldkit-0.3.0}/src/fieldkit/training/__init__.py +0 -0
  33. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/__init__.py +0 -0
  34. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/conftest.py +0 -0
  35. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_capabilities.py +0 -0
  36. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_cli.py +0 -0
  37. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_eval.py +0 -0
  38. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_nim.py +0 -0
  39. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_nim_spark.py +0 -0
  40. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_rag.py +0 -0
  41. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_rag_spark.py +0 -0
  42. {fieldkit-0.2.0 → fieldkit-0.3.0}/tests/test_training.py +0 -0
@@ -6,6 +6,40 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.0] — 2026-05-11
10
+
11
+ Third public release. One new top-level module (`fieldkit.lineage`) lifted from the [auto-research-loop-on-spark article](https://ainative.business/field-notes/auto-research-loop-on-spark/) — the portable part of cxcscmu's *Auto-Research-Recipes* harness, decomposed into a pure-stdlib substrate any harness on the Spark can write into.
12
+
13
+ ### Added — `fieldkit.lineage` (new module)
14
+
15
+ The portable part of cxcscmu's *Auto-Research-Recipes* harness, extracted into a top-level submodule. The case for the primitive is in the released `pg_ablation_lineage_on` vs `pg_ablation_lineage_off` runs: same agent, same prompt template, same 201-trial budget on Parameter Golf — only whether the agent's session prompt includes the rendered lineage block differs. With lineage on: 16 keeps (8.0%), 38 eval-budget overruns. Without: 3 keeps (1.5%), 123 eval-budget overruns. **5.3× more keeps · 3.2× fewer wall-wastes**, with no model change, no compute change, no prompt-template change. ([extract from #auto-research-loop-on-spark])
16
+
17
+ The new module is pure-stdlib (no torch, no numpy) — ~200 LOC of public surface, ~330 LOC including docstrings + renderer helpers.
18
+
19
+ - **`fieldkit.lineage.FailureLabel`** — 10-class string enum (`keep`, `discard`, `crash`, `eval_budget_overrun`, `train_budget_overrun`, `size_blocked`, `preflight_crash`, `harness_abort`, `disqualified`, `baseline`). `.value` round-trips byte-identically to cxcscmu TSVs. The `is_informational` property is the cxcscmu `_QUARANTINED_STATUSES` rule as a method — returns `False` only for `harness_abort` (bookkeeping kills); every other class carries usable signal for the next agent.
20
+ - **`fieldkit.lineage.Trial`** — frozen dataclass for one TSV row. 17 fields in canonical order. `core_metric` is the task-agnostic primary metric (so the module works for Parameter Golf, NanoChat-D12, CIFAR, and any future task in the arc); `val_bpb` is preserved alongside for direct interop with cxcscmu-shaped data. `Trial.header()` / `Trial.to_row()` / `Trial.from_row(dict)` give exact TSV round-trip — `None` floats serialize as empty strings (matches cxcscmu convention).
21
+ - **`fieldkit.lineage.LineageStore(root, *, lower_is_better=True)`** — append-only TSV writer at `root/results.tsv` with `fcntl.flock` exclusive locking across header + row writes (concurrent specialists can write without interleaving). Read-side accessors: `all_trials()`, `latest(n)`, `best()`, `chain_to(exp_id)` (walks `parent_exp` pointers root-first, terminates on missing or self-referential parents), and `render_prompt(...)` — the deterministic Markdown emitter.
22
+ - **`fieldkit.lineage.LineageSnapshot`** — frozen dataclass returned by `render_prompt`. Carries the rendered Markdown string plus the underlying structured data (`current_best`, `chain_to_best`, `top_k_leaderboard`, `recent_n_activity`, `last_m_with_full_hypothesis`) so callers can index in without re-parsing.
23
+ - **`fieldkit.lineage.RecipeEdit`** — pairs a keep trial with its workdir `snapshot_path` and `parent_snapshot_path`. `diff()` computes a unified diff of every text file in the snapshot vs the parent (binary files elide with a `Binary files ... differ` marker); baseline trials with no parent return an empty diff.
24
+
25
+ Rendered Markdown output mirrors cxcscmu's `release_artifacts/example_lineage_pg_lineage_on_arch.txt` shape: header line + `## LEADERBOARD.md` (current best + top-K kept table) + `## KNOWLEDGE.md` (current-best lineage as a nested `└─` chain + recent-activity table + last-M detailed entries). Determinism is tested — same TSV state in produces byte-identical Markdown across calls.
26
+
27
+ ### Test suite
28
+
29
+ **29 new tests** for `fieldkit.lineage` (`tests/test_lineage.py`): `FailureLabel` value parity + `is_informational` predicate + 10-class enum surface lock; `Trial` round-trip via TSV; `LineageStore` append / latest / best / `chain_to` correctness across linear and branched topologies; `render_prompt` determinism, top-K filtering, chain rendering with `← BEST` marker; `RecipeEdit.diff()` against parent snapshots including new-file detection.
30
+
31
+ Total fieldkit test count: **249 passed, 3 skipped** offline (`pytest -q`) — the 3 skips are 1 module-level torch importorskip in `test_training.py` and 2 `--spark`-gated live integration tests.
32
+
33
+ ### Articles in this release
34
+
35
+ - [`auto-research-loop-on-spark`](https://ainative.business/field-notes/auto-research-loop-on-spark/) — anchor article. Walks the 17-column schema, the 10-class enum semantics, and the cxcscmu lineage ablation that proves the primitive's value.
36
+
37
+ ### Schema change — `FIELDKIT_MODULES`
38
+
39
+ `src/content.config.ts` extended to include `'lineage'` in the `FIELDKIT_MODULES` tuple (order: `capabilities, nim, rag, eval, training, lineage, cli`). Required so articles can declare `fieldkit_modules: ['lineage']` in their frontmatter.
40
+
41
+ [extract from #auto-research-loop-on-spark]: https://github.com/manavsehgal/ai-field-notes/tree/main/articles/auto-research-loop-on-spark
42
+
9
43
  ## [0.2.0] — 2026-05-05
10
44
 
11
45
  Second public release. One new module (`fieldkit.training`) plus four extensions to the v0.1 `fieldkit.eval` surface, all lifted from articles in [ai-field-notes](https://ainative.business/field-notes/) — primarily the `clawgym-on-spark` and Frontier Scout arcs. The `fieldkit.agents` and `fieldkit.inference` modules originally targeted for v0.2 are deferred to v0.3+ because their public APIs need a second article's use case to lock in (see "Deferred to v0.3+" below).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fieldkit
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: Verified-on-Spark patterns lifted from the ai-field-notes blog into one importable Python package.
5
5
  Project-URL: Homepage, https://ainative.business/fieldkit/
6
6
  Project-URL: Source, https://github.com/manavsehgal/ai-field-notes/tree/main/fieldkit
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  > Verified-on-Spark patterns lifted from the [ai-field-notes](https://ainative.business/field-notes/) blog into one importable Python package.
41
41
 
42
- Every essay in `ai-field-notes` ends with `evidence/` — a folder of working code that produced the article's numbers. After 25+ articles the same patterns kept reappearing: the same NIM client wrapper, the same chunk-embed-store dance, the same bench harness. `fieldkit` is what those `evidence/` folders look like once the boilerplate is lifted into a real package.
42
+ Every essay in `ai-field-notes` ends with `evidence/` — a folder of working code that produced the article's numbers. After 30+ articles the same patterns kept reappearing: the same NIM client wrapper, the same chunk-embed-store dance, the same bench harness, the same verifier-loop math. `fieldkit` is what those `evidence/` folders look like once the boilerplate is lifted into a real package.
43
43
 
44
44
  The blog stays the long-form rationale. `fieldkit` is the `pip install`-able surface so you can reproduce — and extend — the work without re-pasting 80 lines of NIM-client setup per article.
45
45
 
@@ -52,7 +52,7 @@ pip install fieldkit
52
52
  For the bleeding edge between releases, install from the git tag instead:
53
53
 
54
54
  ```bash
55
- pip install "git+https://github.com/manavsehgal/ai-field-notes.git@fieldkit/v0.1.0#subdirectory=fieldkit"
55
+ pip install "git+https://github.com/manavsehgal/ai-field-notes.git@fieldkit/v0.2.0#subdirectory=fieldkit"
56
56
  ```
57
57
 
58
58
  ## Quickstart
@@ -64,21 +64,32 @@ client = NIMClient(base_url="http://localhost:8000/v1", model="meta/llama-3.1-8b
64
64
  print(client.chat([{"role": "user", "content": "Hello, Spark."}]))
65
65
  ```
66
66
 
67
- ## What's in v0.1.0
67
+ ## What's in v0.2.0
68
68
 
69
69
  | Module | Purpose | Source articles |
70
70
  |---|---|---|
71
71
  | `fieldkit.capabilities` | Typed Python facade over `spark-capabilities.json` — KV cache math, weight bytes, inference envelope. | `kv-cache-arithmetic-at-inference`, `gpu-sizing-math-for-fine-tuning` |
72
72
  | `fieldkit.nim` | OpenAI-compatible NIM client wrapper with retry, chunking, and the 8192-token context guard. | `nim-first-inference-dgx-spark` and friends |
73
73
  | `fieldkit.rag` | `Pipeline(embed_url, rerank_url, pgvector_dsn, generator)` — ingest → retrieve → rerank → fuse. | `naive-rag-on-spark` and friends |
74
- | `fieldkit.eval` | `Bench`, `Judge`, `Trajectory` — the recurring eval harness shapes. | every article with a `bench.py` or `benchmark.py` |
74
+ | `fieldkit.eval` | `Bench`, `Judge`, `Trajectory` — plus v0.2's `AssertionGrader`, `PassAtK`, `AgentRun`, `MatchedBaseComparison`. | every article with a `bench.py` or `benchmark.py`, plus `clawgym-on-spark`, `autoresearchbench-on-spark`, `pass-at-k-after-the-seventh-patch` |
75
+ | `fieldkit.training` *(new in v0.2)* | `LoraReferenceSnapshot` (sidesteps peft 0.19's offloader bug), `WeightDeltaTracker` — for any RL or SFT loop. Lazy `torch` import; pure-inference envs don't pay. | `clawgym-on-spark-grpo` |
75
76
  | `fieldkit.cli` | `fieldkit bench rag`, `fieldkit feasibility <id>`, `fieldkit envelope <size>`. | discoverability |
76
77
 
77
- Modules deferred to `v0.2`: `retriever`, `ft`, `guardrails`, `agents`. To `v0.3`: `train`, `observe`.
78
+ ### What v0.2 adds
79
+
80
+ - **`fieldkit.training`** — new module. `LoraReferenceSnapshot` is a CPU-resident snapshot of a peft adapter's LoRA tensors plus a context manager that swaps the snapshot in for one no-grad forward pass and restores trainable weights on exit. Solves a real peft 0.19 bug: `model.load_adapter(adapter_name="reference", is_trainable=False)` crashes with `KeyError` under `device_map="auto"` whenever the GPU has anything else resident — peft's offload-detection over-triggers on Spark unified memory. `WeightDeltaTracker` is a pre/post snapshot of trainable params with L2 + max|Δ| reporting — sanity-check that any fine-tuning step actually moved weights.
81
+ - **`fieldkit.eval.AssertionGrader`** — pure-function grader over five file-system assertion primitives (`file_exists`, `file_not_exists`, `file_contents_contain`, `file_contents_match_regex`, `file_unchanged`). Lifted from `clawgym-on-spark`'s deterministic grader; no LLM, no fuzzy matching.
82
+ - **`fieldkit.eval.PassAtK` + `pass_at_k_estimator`** — verifier-loop with the Chen 2021 unbiased pass@k estimator (lower variance than the naive `1 - (1-p)^k` for finite n).
83
+ - **`fieldkit.eval.AgentRun` + `TurnDetail` + `summarize_agent_runs`** — per-question agent-bench schema with overrideable field-name path tuples for non-AutoResearchBench layouts.
84
+ - **`fieldkit.eval.MatchedBaseComparison` + `GroupStats`** — two-rollout B−A driver with per-group and per-assertion-kind delta and a markdown `.report()`. Reusable for any LoRA / adapter ablation, fine-tuned-vs-base, or system-prompt-A-vs-B comparison.
85
+
86
+ **Deferred to v0.3+:** `fieldkit.agents` (Persona / WorkspaceSeed / SynthTask / TaskAuthor / Sandbox / RolloutDriver / Trajectory + TurnRecord — 7 symbols), `fieldkit.inference.VLLMClient`, and `replay_messages_from_trajectory`. Each needs a second consuming article before its public API locks.
78
87
 
79
88
  ## Hardware
80
89
 
81
- `v0.1` is **Spark-only**. Every code path is verified on a DGX Spark (GB10, 128 GB unified memory, NIM 8B + embed NIM + pgvector co-resident). Portability to other CUDA 12.x boxes lands in `v0.2+` when there's demand.
90
+ Every code path is verified on a DGX Spark (GB10, 128 GB unified memory, NIM 8B + embed NIM + pgvector co-resident). `fieldkit.training`'s torch + safetensors imports are lazy, so the package costs nothing on inference-only boxes install `torch` and `safetensors` yourself in the training environment when you need the training primitives. NeMo / Triton / pytorch-base containers ship them; pure-inference envs don't.
91
+
92
+ Portability to non-Spark CUDA 12.x boxes lands when there's demand.
82
93
 
83
94
  ## License
84
95
 
@@ -0,0 +1,66 @@
1
+ # fieldkit
2
+
3
+ > Verified-on-Spark patterns lifted from the [ai-field-notes](https://ainative.business/field-notes/) blog into one importable Python package.
4
+
5
+ Every essay in `ai-field-notes` ends with `evidence/` — a folder of working code that produced the article's numbers. After 30+ articles the same patterns kept reappearing: the same NIM client wrapper, the same chunk-embed-store dance, the same bench harness, the same verifier-loop math. `fieldkit` is what those `evidence/` folders look like once the boilerplate is lifted into a real package.
6
+
7
+ The blog stays the long-form rationale. `fieldkit` is the `pip install`-able surface so you can reproduce — and extend — the work without re-pasting 80 lines of NIM-client setup per article.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ pip install fieldkit
13
+ ```
14
+
15
+ For the bleeding edge between releases, install from the git tag instead:
16
+
17
+ ```bash
18
+ pip install "git+https://github.com/manavsehgal/ai-field-notes.git@fieldkit/v0.2.0#subdirectory=fieldkit"
19
+ ```
20
+
21
+ ## Quickstart
22
+
23
+ ```python
24
+ from fieldkit.nim import NIMClient
25
+
26
+ client = NIMClient(base_url="http://localhost:8000/v1", model="meta/llama-3.1-8b-instruct")
27
+ print(client.chat([{"role": "user", "content": "Hello, Spark."}]))
28
+ ```
29
+
30
+ ## What's in v0.2.0
31
+
32
+ | Module | Purpose | Source articles |
33
+ |---|---|---|
34
+ | `fieldkit.capabilities` | Typed Python facade over `spark-capabilities.json` — KV cache math, weight bytes, inference envelope. | `kv-cache-arithmetic-at-inference`, `gpu-sizing-math-for-fine-tuning` |
35
+ | `fieldkit.nim` | OpenAI-compatible NIM client wrapper with retry, chunking, and the 8192-token context guard. | `nim-first-inference-dgx-spark` and friends |
36
+ | `fieldkit.rag` | `Pipeline(embed_url, rerank_url, pgvector_dsn, generator)` — ingest → retrieve → rerank → fuse. | `naive-rag-on-spark` and friends |
37
+ | `fieldkit.eval` | `Bench`, `Judge`, `Trajectory` — plus v0.2's `AssertionGrader`, `PassAtK`, `AgentRun`, `MatchedBaseComparison`. | every article with a `bench.py` or `benchmark.py`, plus `clawgym-on-spark`, `autoresearchbench-on-spark`, `pass-at-k-after-the-seventh-patch` |
38
+ | `fieldkit.training` *(new in v0.2)* | `LoraReferenceSnapshot` (sidesteps peft 0.19's offloader bug), `WeightDeltaTracker` — for any RL or SFT loop. Lazy `torch` import; pure-inference envs don't pay. | `clawgym-on-spark-grpo` |
39
+ | `fieldkit.cli` | `fieldkit bench rag`, `fieldkit feasibility <id>`, `fieldkit envelope <size>`. | discoverability |
40
+
41
+ ### What v0.2 adds
42
+
43
+ - **`fieldkit.training`** — new module. `LoraReferenceSnapshot` is a CPU-resident snapshot of a peft adapter's LoRA tensors plus a context manager that swaps the snapshot in for one no-grad forward pass and restores trainable weights on exit. Solves a real peft 0.19 bug: `model.load_adapter(adapter_name="reference", is_trainable=False)` crashes with `KeyError` under `device_map="auto"` whenever the GPU has anything else resident — peft's offload-detection over-triggers on Spark unified memory. `WeightDeltaTracker` is a pre/post snapshot of trainable params with L2 + max|Δ| reporting — sanity-check that any fine-tuning step actually moved weights.
44
+ - **`fieldkit.eval.AssertionGrader`** — pure-function grader over five file-system assertion primitives (`file_exists`, `file_not_exists`, `file_contents_contain`, `file_contents_match_regex`, `file_unchanged`). Lifted from `clawgym-on-spark`'s deterministic grader; no LLM, no fuzzy matching.
45
+ - **`fieldkit.eval.PassAtK` + `pass_at_k_estimator`** — verifier-loop with the Chen 2021 unbiased pass@k estimator (lower variance than the naive `1 - (1-p)^k` for finite n).
46
+ - **`fieldkit.eval.AgentRun` + `TurnDetail` + `summarize_agent_runs`** — per-question agent-bench schema with overrideable field-name path tuples for non-AutoResearchBench layouts.
47
+ - **`fieldkit.eval.MatchedBaseComparison` + `GroupStats`** — two-rollout B−A driver with per-group and per-assertion-kind delta and a markdown `.report()`. Reusable for any LoRA / adapter ablation, fine-tuned-vs-base, or system-prompt-A-vs-B comparison.
48
+
49
+ **Deferred to v0.3+:** `fieldkit.agents` (Persona / WorkspaceSeed / SynthTask / TaskAuthor / Sandbox / RolloutDriver / Trajectory + TurnRecord — 7 symbols), `fieldkit.inference.VLLMClient`, and `replay_messages_from_trajectory`. Each needs a second consuming article before its public API locks.
50
+
51
+ ## Hardware
52
+
53
+ Every code path is verified on a DGX Spark (GB10, 128 GB unified memory, NIM 8B + embed NIM + pgvector co-resident). `fieldkit.training`'s torch + safetensors imports are lazy, so the package costs nothing on inference-only boxes — install `torch` and `safetensors` yourself in the training environment when you need the training primitives. NeMo / Triton / pytorch-base containers ship them; pure-inference envs don't.
54
+
55
+ Portability to non-Spark CUDA 12.x boxes lands when there's demand.
56
+
57
+ ## License
58
+
59
+ Apache-2.0. See [`LICENSE`](https://github.com/manavsehgal/ai-field-notes/blob/main/fieldkit/LICENSE).
60
+
61
+ ## Links
62
+
63
+ - **Blog:** https://ainative.business/field-notes/
64
+ - **Docs:** https://ainative.business/fieldkit/
65
+ - **Source:** https://github.com/manavsehgal/ai-field-notes/tree/main/fieldkit
66
+ - **Changelog:** [`CHANGELOG.md`](https://github.com/manavsehgal/ai-field-notes/blob/main/fieldkit/CHANGELOG.md)
@@ -1,13 +1,13 @@
1
1
  ---
2
2
  module: cli
3
3
  title: fieldkit (CLI)
4
- summary: A thin Typer wrapper over the four modules. Quick checks and smoke benchmarks without writing Python.
5
- order: 5
4
+ summary: A thin Typer wrapper over the modules. Quick checks and smoke benchmarks without writing Python.
5
+ order: 6
6
6
  ---
7
7
 
8
8
  ## What it is
9
9
 
10
- A thin command-line entry point exposed at `fieldkit` after `pip install`. Every subcommand is a ~20-line wrapper over the existing module APIs — for real workloads, import `fieldkit.{capabilities,nim,rag,eval}` directly instead.
10
+ A thin command-line entry point exposed at `fieldkit` after `pip install`. Every subcommand is a ~20-line wrapper over the existing module APIs — for real workloads, import `fieldkit.{capabilities,nim,rag,eval,training}` directly instead.
11
11
 
12
12
  ## Commands
13
13
 
@@ -17,7 +17,7 @@ Print the installed package version.
17
17
 
18
18
  ```bash
19
19
  $ fieldkit version
20
- 0.1.0.dev0
20
+ 0.2.0
21
21
  ```
22
22
 
23
23
  ### `fieldkit envelope <size>`
@@ -0,0 +1,224 @@
1
+ ---
2
+ module: eval
3
+ title: fieldkit.eval
4
+ summary: Bench, Judge, Trajectory, the project's refusal detector — plus the v0.2 verifier-loop additions (AssertionGrader, PassAtK, AgentRun, MatchedBaseComparison) for agent + RL benchmarks.
5
+ order: 4
6
+ ---
7
+
8
+ ## What it is
9
+
10
+ The eval harnesses the project keeps reinventing: a per-call latency benchmarker that emits the same JSON shape as `articles/*/evidence/benchmark.py`, an LLM-as-judge with the three rubrics from `rag-eval-ragas-and-nemo-evaluator`, a trajectory analyzer for agent-loop JSONL, and a refusal regex catalog unioned across the project's articles.
11
+
12
+ **v0.2 additions** (verifier-loop and agent-bench primitives):
13
+
14
+ - `AssertionGrader` — pure file-system grader over five assertion primitives (`file_exists`, `file_not_exists`, `file_contents_contain`, `file_contents_match_regex`, `file_unchanged`). Lifted from `clawgym-on-spark`'s deterministic grader.
15
+ - `PassAtK` + `pass_at_k_estimator` — verifier-loop with the Chen 2021 unbiased pass@k estimator. Lifted from the `pass-at-k-after-the-seventh-patch` follow-up.
16
+ - `AgentRun` + `TurnDetail` + `summarize_agent_runs` — per-question agent-bench schema with overrideable field-name path tuples for non-AutoResearchBench layouts. Lifted from `autoresearchbench-on-spark`.
17
+ - `MatchedBaseComparison` + `GroupStats` + `MatchedBaseComparisonResult` — two-rollout B−A driver with per-group + per-assertion-kind delta and a markdown `.report()`. Lifted from the `clawgym-on-spark` Phase 5 SFT-vs-base eval.
18
+
19
+ ## Public API
20
+
21
+ ```python
22
+ from fieldkit.eval import (
23
+ # v0.1
24
+ Bench, BenchCall,
25
+ Judge, JudgeResult, JudgeError,
26
+ Trajectory, TrajectoryIter,
27
+ RUBRIC_CORRECTNESS, RUBRIC_FAITHFULNESS, RUBRIC_RELEVANCE,
28
+ BUILTIN_RUBRICS,
29
+ REFUSAL_PATTERNS,
30
+ is_refusal,
31
+ summarize_metric,
32
+
33
+ # v0.2 — assertion grader
34
+ ASSERTION_KINDS,
35
+ AssertionGrader, AssertionResult, GradeResult,
36
+
37
+ # v0.2 — pass@k
38
+ PassAtK, PassAtKResult,
39
+ pass_at_k_estimator,
40
+
41
+ # v0.2 — agent runs
42
+ AgentRun, TurnDetail,
43
+ summarize_agent_runs,
44
+
45
+ # v0.2 — matched-base comparison
46
+ MatchedBaseComparison, MatchedBaseComparisonResult, GroupStats,
47
+ )
48
+ ```
49
+
50
+ ### `Bench(name, metrics, metrics_key=None)`
51
+
52
+ Wall-clock benchmark with numeric metric aggregation. Emits the same `{summary: {...}, calls: [...]}` JSON shape the article evidence files use.
53
+
54
+ ```python
55
+ from fieldkit.eval import Bench
56
+
57
+ with Bench("naive-rag",
58
+ metrics=["embed", "retrieve", "generate_total", "end_to_end"],
59
+ metrics_key="timings_ms") as b:
60
+ b.run(pipe.ask, questions, tag_fn=lambda q: {"kind": classify(q)})
61
+ print(b.report()) # markdown table
62
+ b.dump("benchmark.json") # full JSON
63
+ ```
64
+
65
+ Exceptions in the callable are caught and recorded with `success=False` so a single bad input doesn't sink the sweep. Pass `on_error="raise"` to abort on first failure.
66
+
67
+ ### `Judge(client: NIMClient, rubric=RUBRIC_CORRECTNESS, ...)`
68
+
69
+ LLM-as-judge wrapping any `NIMClient`. Three built-in rubrics: `correctness`, `faithfulness`, `relevance`.
70
+
71
+ ```python
72
+ from fieldkit.eval import Judge
73
+ from fieldkit.nim import NIMClient
74
+
75
+ with NIMClient(base_url="http://localhost:8000/v1",
76
+ model="meta/llama-3.1-8b-instruct") as c:
77
+ judge = Judge.builtin(c, "correctness")
78
+ result = judge.grade(
79
+ question="How much unified memory does the Spark have?",
80
+ prediction="128 GB",
81
+ reference="128 GB",
82
+ )
83
+ print(result.score, result.rationale)
84
+ ```
85
+
86
+ `Judge.parse(raw)` is a static helper that does JSON-then-regex score extraction (handles `{"score": 4, ...}`, fenced ```json blocks, and `"score: 4"` prose forms). Score is `None` iff parsing failed.
87
+
88
+ ### `Trajectory(iters, baseline=None, score_field="val_bpb", lower_is_better=True)`
89
+
90
+ Agent-loop JSONL analyzer. Knob coverage, repeat rate, mode dominance, cumulative best.
91
+
92
+ ```python
93
+ from fieldkit.eval import Trajectory
94
+ traj = Trajectory.from_jsonl(
95
+ "trajectory.jsonl",
96
+ score_field="val_bpb",
97
+ lower_is_better=True,
98
+ )
99
+ traj.knob_coverage() # {knob_name: count, ...}
100
+ traj.repeat_rate() # 0.0 .. 1.0
101
+ traj.mode_dominance() # {mode: fraction, ...}
102
+ traj.cumulative_best() # list[float]
103
+ ```
104
+
105
+ Permissive parser drops malformed lines silently — the agent loop emits intermediate `proposed`/`failed` records too.
106
+
107
+ ### `is_refusal(text) -> bool`
108
+
109
+ Catches "context does not contain the answer", "I do not know", "not specified", and other refusal patterns unioned from `rag-eval-ragas-and-nemo-evaluator` and `lora-on-your-own-qa-pairs`.
110
+
111
+ ### `AssertionGrader()` *(v0.2)*
112
+
113
+ Pure-function grader over five file-system assertion primitives — no LLM, no fuzzy matching, no scoring. The five supported kinds are listed in `ASSERTION_KINDS`; an unknown kind fails the assertion with `"unknown kind: <k>"` rather than crashing the grade.
114
+
115
+ ```python
116
+ from pathlib import Path
117
+ from fieldkit.eval import AssertionGrader
118
+
119
+ grader = AssertionGrader()
120
+ result = grader.grade(
121
+ task, # SynthTask-shaped dict OR bare list
122
+ post_state_root=Path("/tmp/sandbox-N"),
123
+ )
124
+ print(result.passed, result.n_passed, result.n_total)
125
+ ```
126
+
127
+ `task` accepts either a SynthTask-shaped dict (must have `verifiable_assertions`; may have `task_id` and `workspace_seed.files`, the latter auto-populates `seed_files` for `file_unchanged` checks) or a bare list of assertion dicts (each with `kind`, `path`, plus kind-specific keys like `must_contain` / `regex`). Pass `seed_files=` explicitly to enforce `file_unchanged`; without it those assertions report "skipped (no seed content)" and count as pass.
128
+
129
+ `GradeResult` is JSON-serializable via `.to_dict()` and carries per-assertion outcomes plus the binary AND across all assertions. `AssertionResult.detail` is empty on pass; on failure it records the proximate cause (missing path, regex did not match, divergent contents, etc.) so a grade dump is debuggable without re-running the rollout.
130
+
131
+ ### `PassAtK(ks=(1,))` and `pass_at_k_estimator(n, c, k)` *(v0.2)*
132
+
133
+ Verifier-loop primitive: pass@k from per-task n-sample grades, using the **Chen et al. (2021) unbiased estimator** `1 - C(n-c, k) / C(n, k)`. Lower variance than the naive `1 - (1-p)^k` for finite n; the naive form silently over-estimates when c is small relative to n.
134
+
135
+ ```python
136
+ from fieldkit.eval import PassAtK
137
+
138
+ pak = PassAtK(ks=(1, 8))
139
+ result = pak.score(
140
+ problems=[{"task_id": "HumanEval/0", "test": "...", ...}, ...],
141
+ samples=[["sample1", "sample2", ...], ...], # K per problem
142
+ grader=lambda text, problem: humaneval_run(text, problem),
143
+ )
144
+ print(result.pass_at) # {1: 0.7050, 8: 0.8415}
145
+ ```
146
+
147
+ `samples` is a sequence-of-sequences with one fixed sample count across problems; `PassAtK.score` raises if they diverge. `extras_fn(problem, samples) -> dict` is an optional hook for attaching per-problem metadata (first-sample tail, decode-token counts, etc.) onto each `per_task` row without bloating the grader interface.
148
+
149
+ When you've already graded the rollout offline (e.g. you have a `comparison.json` from a prior bench), use `pak.from_rows(rows)` with pre-counted `(task_id, n, passed)` triples to skip re-grading.
150
+
151
+ The standalone `pass_at_k_estimator(n, c, k)` is exported separately for callers who already have `(n, c)` rows.
152
+
153
+ ### `AgentRun` + `TurnDetail` + `summarize_agent_runs(runs)` *(v0.2)*
154
+
155
+ Canonical schema for any third-party agent bench that emits a per-question record with a status, total wall time, and a list of turn dicts. Covers AutoResearchBench, autoresearch-agent-loop, and clawgym-on-spark rollouts out of the box; field-name path tuples on `from_record` cover the rest.
156
+
157
+ ```python
158
+ from fieldkit.eval import AgentRun, summarize_agent_runs
159
+
160
+ runs = AgentRun.from_jsonl(
161
+ "evidence/runs/llama-3.1-8b/inference_output.jsonl"
162
+ )
163
+ print(summarize_agent_runs(runs, label="llama-3.1-8b"))
164
+
165
+ # Custom bench shape — override the path tuples
166
+ custom = AgentRun.from_record(
167
+ raw,
168
+ question_id_field="task_id",
169
+ question_id_path=(), # top-level
170
+ inference_path=("result",), # not inference_results[0]
171
+ turns_field="trace",
172
+ )
173
+ ```
174
+
175
+ `TurnDetail` keeps five canonical fields (`turn`, `action`, `duration_s`, `input_tokens`, `output_tokens`) and stuffs everything else from the source record into `extras` so the canonical accessors stay stable while bench-specific fields (`papers_retrieved`, `parse_errors`, `candidate_cfg`) survive round-tripping.
176
+
177
+ Convenience accessors on `AgentRun` are pure derivations of `turns`: `tool_calls()` (action == "tool"), `tool_format_errors()` (action == "error"), `total_input_tokens()`, `total_output_tokens()`, `succeeded()` (status == "finished" AND ≥1 candidate). Override `succeeded()` for benches with different success semantics.
178
+
179
+ `summarize_agent_runs(runs, label="...")` aggregates per-status counts plus `summarize_metric` rollups for `wall_seconds`, `turns`, `candidates`, `tool_calls`, `tool_format_errors`. Mirrors the JSON shape `articles/autoresearchbench-on-spark/scripts/analyze_run.py` writes — pass straight to `json.dumps`.
180
+
181
+ ### `MatchedBaseComparison(group_extractor=...)` *(v0.2)*
182
+
183
+ Two-rollout B−A comparison over a held-out task set. The "filter held-out by training-set membership → run rollout twice with different `--model` → emit B − A comparison" pattern is reusable for any LoRA / adapter ablation — GRPO-vs-SFT, fine-tuned-vs-base, system-prompt-A-vs-B.
184
+
185
+ Trajectory record schema (one dict per task):
186
+
187
+ ```json
188
+ {
189
+ "task_id": "synth-<persona>-NN",
190
+ "final_grade": {
191
+ "passed": true,
192
+ "n_passed": 3,
193
+ "n_total": 3,
194
+ "assertions": [{"kind": "file_exists", "passed": true}, ...]
195
+ },
196
+ "stopped": "task_complete",
197
+ "n_turns": 5,
198
+ "wall_seconds": 12.3
199
+ }
200
+ ```
201
+
202
+ ```python
203
+ from fieldkit.eval import MatchedBaseComparison
204
+ import json
205
+
206
+ cmp = MatchedBaseComparison()
207
+ result = cmp.compare(
208
+ baseline=base_trajectories, # list of dicts OR path/JSONL
209
+ candidate=sft_trajectories,
210
+ )
211
+ print(result.report()) # markdown headline + per-group + per-kind
212
+ json.dump(result.to_dict(), open("comparison.json", "w"), indent=2)
213
+ ```
214
+
215
+ `group_extractor` defaults to a synth-persona splitter (`synth-data-science-researcher-03 → data-science-researcher`); pass any `Callable[[str], str]` for arxiv-id prefixes, Bench question categories, or other task-id schemes. Set to `None` to disable per-group breakdown.
216
+
217
+ `GroupStats` aggregates one rollout: total + per-passed task counts, per-assertion totals, `by_group` and `by_kind` buckets, stop-reason histogram, mean turns, mean wall. `MatchedBaseComparisonResult.overall_delta` carries the headline four numbers — task and per-assertion deltas in percentage points, plus mean-turns and mean-wall deltas. `.report()` renders a markdown summary table; `.to_dict()` serializes the full comparison for `comparison.json` files.
218
+
219
+ `MatchedBaseComparison.stats(rows)` is exposed separately when you only need single-rollout aggregation (no comparison). Accepts a list/iterable of dicts or a JSONL path.
220
+
221
+ ## Samples
222
+
223
+ - [`samples/bench-rag.py`](https://github.com/manavsehgal/ai-field-notes/blob/main/fieldkit/samples/bench-rag.py) — offline `Bench` + `Judge.parse` walkthrough.
224
+ - [`articles/naive-rag-on-spark/evidence/benchmark.py`](https://github.com/manavsehgal/ai-field-notes/blob/main/articles/naive-rag-on-spark/evidence/benchmark.py) — the original article's benchmark, rewritten on top of `fieldkit.eval.Bench`. Reproduces the same behavioral fingerprint: 5 of 6 refusals (incl. the canonical Google-IPO false refusal) plus the Ian Thorpe grounded answer.
@@ -0,0 +1,118 @@
1
+ ---
2
+ module: lineage
3
+ title: fieldkit.lineage
4
+ summary: Append-only trial log + deterministic prompt rendering — the portable part of cxcscmu's Auto-Research-Recipes harness. A 17-column TSV per trial, a 10-class status enum, and the Markdown lineage block the next specialist reads at session entry.
5
+ order: 6
6
+ ---
7
+
8
+ ## What it is
9
+
10
+ The release_artifacts pattern from cxcscmu's *Auto-Research-Recipes* harness, decomposed into Python. Four dataclasses, one enum, ~200 lines of pure-stdlib code — and a determinism contract: same TSV state in, same rendered Markdown out.
11
+
12
+ The case for the primitive sits in cxcscmu's own `pg_ablation_lineage_on` vs `pg_ablation_lineage_off` runs. Same agent. Same prompt template. Same 201 trials of search budget on Parameter Golf. Same Claude Opus on each specialist. The only difference is whether the agent's session prompt includes the rendered lineage block. With lineage on: 16 keeps (8.0%), 38 eval-budget overruns (19%), best `val_bpb` 1.073142. With lineage off: 3 keeps (1.5%), **123 eval-budget overruns (61%)**, best `val_bpb` 1.077413. **5.3× more keeps · 3.2× fewer wall-wastes · 0.004 val_bpb deeper.** The intervention isn't the agent. The intervention is letting the agent see what was tried.
13
+
14
+ `fieldkit.lineage` is the portable substrate that lets you give that intervention to your own loops — no model weights, no GPUs, no NIM containers, no Claude budget. A TSV writer with `fcntl.flock` for concurrent specialist writes, a small enum, a deterministic Markdown renderer.
15
+
16
+ ## Public API
17
+
18
+ ```python
19
+ from fieldkit.lineage import (
20
+ FailureLabel,
21
+ Trial,
22
+ RecipeEdit,
23
+ LineageSnapshot,
24
+ LineageStore,
25
+ )
26
+ ```
27
+
28
+ ### `FailureLabel`
29
+
30
+ String-valued enum with 10 classes; `value` round-trips identically to cxcscmu's TSV `status` column.
31
+
32
+ | value | meaning |
33
+ |---|---|
34
+ | `keep` | Trial ran to completion, improved the leaderboard, snapshot archived |
35
+ | `discard` | Trial ran to completion, didn't improve — informational, the clean failure mode |
36
+ | `crash` | Trial died mid-run (exception, OOM, NCCL error) |
37
+ | `eval_budget_overrun` | Trained inside budget, eval phase exceeded its wall — partial signal |
38
+ | `train_budget_overrun` | Training phase exceeded its wall |
39
+ | `size_blocked` | Killed by an artifact-size constraint |
40
+ | `preflight_crash` | Died before the trial proper started (infrastructure) |
41
+ | `harness_abort` | Bookkeeping kill (the only non-informational class) |
42
+ | `disqualified` | Vision-side: completed but failed a structural gate (CIFAR) |
43
+ | `baseline` | The seed every run starts from |
44
+
45
+ The `is_informational` property returns `False` only for `harness_abort` — everything else carries signal for the next agent.
46
+
47
+ ### `Trial`
48
+
49
+ Frozen dataclass for one TSV row. 17 fields in canonical order: `exp_id`, `timestamp`, `specialist`, `parent_exp`, `baseline_exp`, `domain`, `hypothesis`, `expected_delta`, `status`, `core_metric`, `val_bpb`, `delta_vs_best`, `train_s`, `total_s`, `job_name`, `snapshot_path`, `notes`.
50
+
51
+ `core_metric` is the task-agnostic primary metric — for language-model runs it mirrors `val_bpb`; for vision tasks it carries top-1 error or whatever the leaderboard sorts on. The duplicated `val_bpb` column is preserved for direct interop with cxcscmu-shaped TSVs.
52
+
53
+ ```python
54
+ Trial.header() # canonical TSV header (17 field names in order)
55
+ trial.to_row() # ['000', '2026-05-11T10:00:00Z', 'baseline', ...]
56
+ Trial.from_row(rowdict) # parse one csv.DictReader row back to a Trial
57
+ ```
58
+
59
+ ### `LineageStore(root, *, lower_is_better=True)`
60
+
61
+ Append-only TSV writer at `root/results.tsv` plus read-side accessors. All writes hold an exclusive `fcntl.flock` across the header-write + row-write sequence, so multiple specialists can write concurrently without interleaving.
62
+
63
+ ```python
64
+ from pathlib import Path
65
+ from fieldkit.lineage import LineageStore, Trial, FailureLabel
66
+
67
+ store = LineageStore(Path("magent_state/blackboard"))
68
+ store.append(Trial(exp_id="000", ..., status=FailureLabel.BASELINE, ...))
69
+
70
+ store.all_trials() # list[Trial] in insertion order
71
+ store.latest(n=30) # tuple[Trial, ...] most recent
72
+ store.best() # Trial | None — best informational row by core_metric
73
+ store.chain_to("014") # tuple[Trial, ...] root-first, walking parent_exp
74
+ ```
75
+
76
+ ### `LineageStore.render_prompt(for_specialist, *, top_k=20, recent_n=30, last_m_full=10, session_timestamp="")`
77
+
78
+ The deterministic Markdown renderer. Returns a `LineageSnapshot` carrying both the rendered string and the underlying structured data (so callers can index in without re-parsing). Output mirrors cxcscmu's `release_artifacts/example_lineage_pg_lineage_on_arch.txt` shape: header line, `## LEADERBOARD.md` (current best + top-K kept table), `## KNOWLEDGE.md` (current-best lineage as a nested `└─` chain + recent-activity table + last-M detailed entries).
79
+
80
+ ```python
81
+ snap = store.render_prompt(
82
+ for_specialist="opt",
83
+ top_k=20,
84
+ recent_n=30,
85
+ last_m_full=10,
86
+ session_timestamp="2026-05-11T11:00:00Z",
87
+ )
88
+ print(snap.rendered_prompt) # the Markdown block
89
+ snap.current_best # the Trial it pointed at
90
+ snap.chain_to_best # tuple[Trial, ...] root → best
91
+ snap.top_k_leaderboard # tuple[Trial, ...] sorted by core_metric
92
+ ```
93
+
94
+ ### `RecipeEdit`
95
+
96
+ Frozen dataclass pairing a keep trial with its workdir snapshot and the parent snapshot. `diff()` computes a unified diff of every text file in the snapshot vs the parent on first call (binary files emit a `Binary files ... differ` marker).
97
+
98
+ ```python
99
+ edit = RecipeEdit(
100
+ trial=keep_trial,
101
+ snapshot_path=Path("snapshots/014_opt"),
102
+ parent_snapshot_path=Path("snapshots/000_baseline"),
103
+ )
104
+ print(edit.diff()) # unified diff a/train.py → b/train.py
105
+ ```
106
+
107
+ The baseline trial returns an empty diff (no parent).
108
+
109
+ ## Why this surface
110
+
111
+ Three things to notice about the shape. First, `FailureLabel.is_informational` is the cxcscmu `_QUARANTINED_STATUSES` rule made into a method — any downstream consumer can read it without re-implementing the policy. Second, `LineageSnapshot` is a record of *what the agent saw* (including the rendered prompt), not just a reference to the underlying TSV state. That matters for reproducibility: if you want to know why the agent at iteration 178 made the choice it did, you read the snapshot, not the TSV. Third, `LineageStore.render_prompt` is the same deterministic function cxcscmu's `harness/blackboard.py` implements (~600 lines of careful Markdown assembly); the `fieldkit.lineage` version is the published, testable, pure-stdlib port.
112
+
113
+ The module lands at the top level of `fieldkit` because lineage is task-agnostic. Parameter Golf uses it. NanoChat-D12 uses it. CIFAR uses it — and its `disqualified` class is the evidence that this primitive isn't language-model-specific. Putting it under `fieldkit.training` would suggest LM specificity that isn't there.
114
+
115
+ ## Samples
116
+
117
+ - [`samples/hello-lineage.py`](https://github.com/manavsehgal/ai-field-notes/blob/main/fieldkit/samples/hello-lineage.py) — five-trial worked example: baseline, two keeps, one discard, one `eval_budget_overrun`. Prints the rendered prompt.
118
+ - [`articles/auto-research-loop-on-spark/`](https://ainative.business/field-notes/auto-research-loop-on-spark/) — anchor article. Walks the 17-column schema, the 10-class enum semantics, and the `pg_ablation_lineage_on/off` ablation that proves the primitive's value.
@@ -0,0 +1,85 @@
1
+ ---
2
+ module: training
3
+ title: fieldkit.training
4
+ summary: Fine-tuning primitives for any RL or SFT loop on the Spark — a CPU-resident LoRA reference snapshot that sidesteps peft 0.19's offloader bug, and a pre/post weight-delta tracker for sanity-checking that gradients actually moved.
5
+ order: 5
6
+ ---
7
+
8
+ ## What it is
9
+
10
+ Two utilities lifted from `articles/clawgym-on-spark` for any PPO / GRPO / DPO / SFT loop on the DGX Spark's unified-memory GB10:
11
+
12
+ - **`LoraReferenceSnapshot`** — a CPU-resident snapshot of a peft adapter's LoRA tensors plus a context manager that swaps the snapshot into the live model for one no-grad forward pass and restores trainable weights on exit. **Solves a real peft 0.19 bug**: `model.load_adapter(adapter_name="reference", is_trainable=False)` crashes with a `KeyError` under `device_map="auto"` whenever the GPU has anything else resident — peft's offload-detection over-triggers on Spark unified memory. Verified with vLLM co-resident *and* with the trainer alone. The snapshot/swap dance sidesteps the offloader entirely.
13
+ - **`WeightDeltaTracker`** — pre/post snapshot of trainable params with L2 + max|Δ| reporting. Sanity-check that any fine-tuning step actually moved weights. The first time someone debugs "why didn't my LoRA update?" they'll wish for this.
14
+
15
+ Both classes use **lazy `torch` imports** so `import fieldkit.training` costs nothing in environments that don't run training. Construct any class and you'll get a clear `ImportError` if `torch` (or `safetensors`, for `LoraReferenceSnapshot.from_disk`) isn't installed — install them yourself in the training environment. NeMo / Triton / pytorch-base containers ship them; pure inference envs don't.
16
+
17
+ ## Public API
18
+
19
+ ```python
20
+ from fieldkit.training import (
21
+ LoraReferenceSnapshot,
22
+ WeightDeltaTracker,
23
+ )
24
+ ```
25
+
26
+ ### `WeightDeltaTracker(model)`
27
+
28
+ Snapshot every parameter for which `requires_grad` is True at construction time, copy to CPU. `delta()` re-reads the live model and computes aggregate L2 + max-abs-delta against the snapshot.
29
+
30
+ ```python
31
+ from fieldkit.training import WeightDeltaTracker
32
+
33
+ tracker = WeightDeltaTracker(model)
34
+ # ... one or more optimizer steps ...
35
+ l2, max_abs = tracker.delta()
36
+ print(f"weight L2 = {l2:.6f}, max|Δ| = {max_abs:.6f}")
37
+ ```
38
+
39
+ `delta()` returns `(0.0, 0.0)` when no trainable params were captured (the model was set to inference mode before construction). Tensors that became trainable *after* construction are ignored — the tracker only re-measures what it captured.
40
+
41
+ `len(tracker)` returns the number of tensors held in the pre-snapshot. ~15 lines of math, lazy-torch import.
42
+
43
+ ### `LoraReferenceSnapshot(model, *, snapshot=None)`
44
+
45
+ A context manager that swaps a CPU-resident snapshot's LoRA weights into the live model for one no-grad forward pass, then restores the pre-swap trainable values on exit. Default constructor snapshots the model's *current* trainable params (online-reference flavor); pass `snapshot=` directly to reuse one snapshot dict across many model instances.
46
+
47
+ ```python
48
+ from fieldkit.training import LoraReferenceSnapshot
49
+
50
+ # Online — snapshot current policy at step start
51
+ snap = LoraReferenceSnapshot(model)
52
+ # ... one or more optimizer steps on the policy ...
53
+ with snap:
54
+ ref_logits = model(input_ids).logits # frozen-policy forward
55
+ # trainable weights restored on exit
56
+ ```
57
+
58
+ ### `LoraReferenceSnapshot.from_disk(model, adapter_dir, *, adapter_name="default", weights_filename="adapter_model.safetensors")`
59
+
60
+ Load LoRA weights from a peft adapter directory on disk. Performs the **safetensors-key transform** required by peft: keys in the file have shape `base_model.<…>.weight` while live parameters have shape `base_model.<…>.<adapter_name>.weight`. The snapshot indexes live names so swap/restore Just Works.
61
+
62
+ ```python
63
+ # Fixed reference — classic GRPO with SFT-init reference policy
64
+ snap = LoraReferenceSnapshot.from_disk(
65
+ model,
66
+ adapter_dir="adapters/sft-init",
67
+ adapter_name="default",
68
+ )
69
+ for step in range(num_steps):
70
+ with snap:
71
+ ref_logits = model(...).logits
72
+ # ... policy update against fixed reference ...
73
+ ```
74
+
75
+ Names that don't match the live model's trainable params are silently skipped — the loader is tolerant of LoRA targets that vary between the saved adapter and the live one (a common occurrence when adapters load into a slightly different model build).
76
+
77
+ `len(snap)` returns the number of LoRA tensors in the snapshot. Nested `with` is rejected with a `RuntimeError` — only one swap can be active at a time.
78
+
79
+ ## Why it's only two classes
80
+
81
+ The `clawgym-on-spark` GRPO training loop (`articles/clawgym-on-spark/scripts/grpo_train.py`) leaned on these two patterns repeatedly. They're the smallest, most-grounded surface that survived the v0.2 extract review — anything broader (a full trainer wrapper, an `RLConfig`, a peft-side adapter loader) needs a second consuming article before the API locks. Look out for them in subsequent off-policy-training pieces; the v0.3 release is where larger training surfaces will land.
82
+
83
+ ## Samples
84
+
85
+ - [`articles/clawgym-on-spark/scripts/grpo_train.py`](https://github.com/manavsehgal/ai-field-notes/blob/main/articles/clawgym-on-spark/scripts/grpo_train.py) — the original `--reference-adapter` + snapshot/swap blocks and the `--check-weight-delta` harness this module is lifted from.