Repository navigation
feat(core)!: Model.extra as a capability contract, and a code-based preprocess identity - #221
Merged
Merged
Conversation
…reprocess identity
A model trained with special tokens around biological sequences, such as
NatureLM or a domain-tokenizer model, needs them at inference, or its
tokenizer reads the sequence as plain text and the score degrades without an
error. The wrapper format belongs to the model; only the task knows which
text is which sequence type.
- Model.extra is documented as the contract between tasks and models: a task
asks for a capability by key, an absent key changes nothing, and core
interprets no key. The first key is sequence_wrappers, {dna|rna|protein|mol:
"<format with {seq}>"}. A template without {seq}, with another field, or
under an unknown type is refused when the model is built and by the config
check. A task reads self.model.extra.get("sequence_wrappers", {}) in
preprocess; no built-in task injects such sequences yet.
- preprocess is identified by code, as postprocess, feedback and report are:
the static walk from preprocess, the grader_args() it reads, and a digest
of every split but test when it reads one (few-shot exemplars). It is
computed once at setup; no sample is preprocessed for it. A rendered prompt
is code, args, data and model capabilities; the last reach the request,
which inference already digests per sample. So one benchmark on a wrapping
and a plain model shares data and preprocess and differs only in inference.
- A run-time name or a file opened at run time makes preprocess count the
task's whole code and the data files beside the reading module, recorded
in fallback. A module read at import that loads a file beside it adds that
package's data files without falling back.
- A preprocess change restarts every sample of the task, where before only
samples whose prompt changed restarted. Each preprocess record is stamped
with the preprocess key; a run interrupted mid-restart leaves preprocess
unknown, and each sample's stamp decides.
- IDENTITY_SCHEMA is 3; RESULT_SCHEMA_VERSION stays 1. A schema-1 or schema-2
directory recorded rendered-prompt digests: a resume renders each recorded
sample once and upgrades in place when every one renders what it recorded,
else counts preprocess as changed. The pass runs only on that upgrade.
task_resume_plan reports legacy_preprocess for such a directory.
- The dry-run's per-task stages compare preprocess (no longer unknown): a
wrapping and a plain model show it same, a preprocess change shows it
differs with rerun_from preprocess, and a schema-1 or schema-2 directory
carries the verdict of the planning pass.
Closes #218
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only the named few-shot split Review follow-ups on the code-based preprocess identity. - A task that overrides grader_args may keep an argument under another attribute name (self._k for n_shot), which the walk cannot match, so a change to it left preprocess unchanged. Such a task now counts every argument grader_args returns. No built-in task overrides it. - When preprocess reads a split and names it through fewshot_split, only that split is digested; otherwise every split but test, as before. - Markdown beside a reading module is data (a prompt.md template); only AGENTS.md, CLAUDE.md and README.md are skipped. - Model.extra is typed dict[str, JSONValue] again; sequence_wrapper(extra, type) reads a template for tasks. - The duplicate _SPLIT_READS is gone; the walk's known blind spots (environment variables, readers outside _FILE_READS) are documented. - Docs, CHANGELOG and docstrings trimmed; the schema-2 and schema-3 migration bullets are one, since no release wrote schema 2. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ethan-scitix
force-pushed
the
feat/sequence-wrappers
branch
from
October 23, 2026 11:19
55c47a3 to
71dd764
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Type
Summary
Models trained with special tokens around biological sequences (NatureLM, domain-tokenizer models:
<dna>ACGT</dna>) need those tokens at inference. Without them the tokenizer reads the sequence as plain text and the score drops silently; in one case a DNA classification fell to about chance. The wrapper format belongs to the model, but only the task knows which text is which sequence type. This PR makesModel.extrathe contract between them, and identifiespreprocessby code, so the wrapping shows up only ininference.1.
Model.extrais the capability contract (sieval/core/models/extra.py)A task asks the model for a capability by key; an absent key changes nothing. Core interprets no key and only checks the shape of documented ones, so a typo fails before the run.
First key:
sequence_wrappers: {dna|rna|protein|mol: "<format with {seq}>"}. A template without{seq}, with another field, or under an unknown type is refused when the model is built and by--dry-run.sequence_wrapper(extra, type)reads a template. No built-in task injects biological sequences, so none is wired here. A plugin task:2.
preprocessis identified by code, not by rendered prompts. It uses the static walk the grading stages use, computed once at setup.preprocessreaches, and thegrader_args()it readspreprocess({digest, modules, args})data, andpreprocess.splitsextra)inference, whose per-samplerequestsdigest the body as sentdataandpreprocessand differs only ininference. Reusing one model's inference for the other is refused, as before.preprocessreads (asself.<name>,self._<name>, or an attribute stored from one) count. A task that overridesgrader_argsmay keep an argument under another name, so there every argument it returns counts. No built-in task overrides it.fewshot_splitargument names is digested; without that argument, every split buttest. Digesting 20k rows takes about 0.1s.preprocessreaches code through a run-time name (getattr,eval,importlib), or a function it calls opens a file, the identity covers the task's whole code plus the data files beside the reading module, withfallbackrecording why. Among built-in tasks only Spider 2.0-lite and QuoteBench take this path. Modules that read a file beside them at import (LiveCodeBench few-shot JSON, NL2SH assets, SciCode, AdvancedIF) add that package's data files without falling back. Markdown counts as data; onlyAGENTS.md,CLAUDE.mdandREADME.mdare skipped.pd.read_csv), is covered only through the argument naming it.preprocesschange now restarts every sample of the task; before, only the samples whose rendered prompt changed. Eachpreprocessrecord is stamped with the key, so an interrupted restart leavespreprocessunknown and each sample's stamp decides.IDENTITY_SCHEMA3;RESULT_SCHEMA_VERSIONstays 1). Schemas 1 and 2 recorded per-sample rendered-prompt digests. On resume, ifdatamatches and every recorded sample re-renders the prompt it recorded, the block is upgraded in place; otherwisepreprocesscounts as changed. That costs one localpreprocesscall per recorded sample, during the upgrade only. I chose this over accepting by inference request digests: those are missing underrecord_meta: false, and they fold the prompt together with everything else sent, so a mismatch cannot name the stage.plan_run'sstages(feat(cli): sieval run --dry-run reports each task's stage plan #220) andtask_resume_plannow comparepreprocessinstead of leaving it out. A preprocess change givesrerun_from: preprocess;legacy_preprocess: truemarks a schema-1 or schema-2 directory, andplan_runcarries the verdict of its rendering pass. feat(cli): sieval run starts an engine only for samples left to infer #216's lazy launch reads the same plan.Breaking change
meta.jsonidentity.schemais 3. Itspreprocessis{digest, modules, args, splits?, fallback?}. Readers must still accept schema 1 (and schema 2, written only by unreleasedmainbuilds), whosepreprocess/promptis{digest, samples}. The CHANGELOG now carries one migration entry for schemas 2 and 3, since no release wrote schema 2.preprocessrecord's stamp is{data, preprocess}instead of{data, prompt}.stage_code()takesargs=and itspreprocessentry gainsargs,splitsandfiles.ResumePlangainslegacy_preprocess.recorded_sample_digests()returns aSampleDigestsmapping.Related Issues
Closes #218
Test Plan
Automated
ruff check && ruff format --check)ty check)main(pytest tests/unit tests/integration -n 8 -m "not stress" --cov --cov-fail-under=95): 9623 passed, 1 skipped; coverage 97.16% (gate 95%).test_exec_quotebench_anchor.pyon macOS;test_no_memory_leak_across_runsandtest_benchmark_scenariosunder-n 8only, which pass serially).pre-commit run -a,scripts/check_preflight.pyandscripts/sync_package_stubs.py --checkpass.New tests:
tests/unit/core/runners/test_sequence_wrappers.pyuses a dual-sequence DNA task and two models, one withsequence_wrappersand one without.dataandpreprocessare equal;inferenceand itsrequestsdiffer, and the request bodies show<dna>…</dna>around both sequences.inference, in botharunandplan_run;task_resume_planandplan_run.stagesshowpreprocess: same.preprocess; withfewshot_splitnamed, only that split is digested and an unread split's rows do not move it.sequence_wrapperand the validation refusals are covered.tests/unit/core/tasks/test_run_meta_preprocess.pyruns on a plugin package on disk.preprocess, to a helper it calls, or to a module constant changes the identity; an edit toreportorfeedbackdoes not.preprocessreads changes it, one it does not read leaves it, and under agrader_argsoverride an argument kept under another name still changes it.prompt.mdtemplate counts whileCLAUDE.mddoes not; a computedgetattrfalls back.tests/unit/core/runners/test_stage_resume.py:preprocesszero times.preprocessunknown, and the next run infers only the samples still stamped with the old key.plan_runreports both.tests/integration/resume/test_across_releases.py: the 0.10.0, 0.10.2 and 0.11.0 fixture directories resume into schema 3 with zero restarts, regrades and model calls, and the upgradedpreprocessequals a fresh run's. A grader argument change still regrades without restarting; a prompt change restarts all 5 samples.tests/unit/cli/test_validation.py: a malformedsequence_wrappersunder a model'sextra,args.extraor a task'sinfer_args.extrafails the config check.Each fix from review was checked by reverting it: the
grader_argsoverride, Markdown-as-data and named-split tests each fail without their fix.Manual
preprocesswalk was listed. Few-shot tasks pick upsplitsand only the arguments their prompt reads (HumanEval'stimeoutandkstay out). The tasks above fall back or add package data files. No built-in task overridesgrader_args.Checklist
Required (all PRs)
type(scope): description)AI-Generated Code - <model> (<provider>)in module docstringcore/If: Breaking Change
🤖 Generated with Claude Code