Python API
The Python API is the set of extension points a learning method plugs into. Reef owns everything around them: accepting and replaying records, holding a batch until it is acknowledged, committing algorithm state, running the backend, and publishing the next version.
Write a recipe is the executable tutorial.
What to implement #
Start with Recipe and add only what the method actually needs.
|
Need |
Component |
Required for |
|---|---|---|
|
configure deployment behavior |
every deployment | |
|
validate feedback at ingress |
methods whose signal arrives in reports | |
|
turn records into a typed batch |
every method producing updates | |
|
carry data to the backend |
every method producing updates; subclass only for a new shape | |
|
turn a batch into a signal |
weight-training methods | |
|
deliver a new artifact medium |
only a new evolution medium | |
|
propose a harness edit and grade an episode |
harness-evolution methods | |
|
gate a produced candidate |
optional, any recipe | |
|
a new tensor objective |
rarely |
Method code should depend only on what this page documents. Anything else under reef. is an implementation detail and may change.
Recipe #
from reef.recipe import Recipe, WeightTrainingRecipe, config_field
from reef.train.cordis_backend import CordisRecipeA recipe is one frozen dataclass configuring the serving and evolution behavior for every scenario in a deployment.
Recipe record-only by default reef.recipe
├── WeightTrainingRecipe step preparer, loss family, TrainingRuntime
│ ├── SAORecipe recipes.sao.recipe
│ ├── TTTDRecipe recipes.tttd.recipe
│ └── OpenClawRLRecipe recipes.openclawrl.recipe
└── CordisRecipe harness tree + episodes reef.train.cordis_backend
└── SkillClawRecipe recipes.skillclaw.recipeChoose the narrowest class whose assumptions all hold. Inheriting Recipe starts without the extra contracts of a specialized base; it does not force the recipe to remain record-only.
|
What you are building |
Where to start |
|---|---|
|
record traffic, no updates |
Recipe as-is |
|
train and publish weights |
subclass WeightTrainingRecipe |
|
use Reef's harness loop |
configure CordisRecipe; subclass it only for a named preset or extra validation |
|
evolve a different artifact |
subclass Recipe, override build() and build_surface() |
|
serve an externally produced artifact |
subclass Recipe, override build_surface() only |
Common members #
|
Member |
Type |
Contract |
|---|---|---|
|
name |
str |
instance field; the default registry key |
|
runtime |
InferenceRuntime | None |
narrowed to a required TrainingRuntime by WeightTrainingRecipe |
|
checkpoint_strategy |
CheckpointStrategy |
defaults to EveryNVersions(1) |
|
build(scenario, records, algorithm_state=...) |
Trainer |
construct the scenario trainer |
|
build_surface(scenario) |
Surface |
the delivery contract for one named scenario |
|
build_artifact_validator() |
ArtifactValidator |
artifact admission, enforced before publication and rollback; defaults to AcceptAnyArtifact() |
|
serving_status() |
Mapping | None |
runtime-wide state for /reef/status |
Every recipe may declare report_type, the ReportBase subclass its reports parse as (None keeps ingress open). Weight-training recipes add training_spec(), which binds the processor, the registered or dotted step preparer, and the backend loss family; max_staleness, the accepted producing-to-serving version lag, which must match the runtime; and candidate_evaluation, the optional plugin configured by the deployment's evaluation section.
@dataclass(frozen=True, kw_only=True)
class MyMethodRecipe(WeightTrainingRecipe):
name: str = "my_method"
@property
def report_type(self) -> type[MyMethodReport]:
return MyMethodReport
@classmethod
def training_spec(cls) -> WeightTrainingSpec:
return WeightTrainingSpec(
processor=MyMethodProcessor,
step_preparer="my_method.prepare:prepare_step",
loss_family="my_method",
)frozen=True is required by the base. kw_only=True keeps later fields keyword-only while the training runtime stays the positional dependency. Call super().__post_init__() first when adding validation.
Configuration #
config_field() is a dataclasses.field carrying a default, a type-aware parser, and an optional environment fallback:
batch_size: int = config_field(4, env="REEF_MY_METHOD_BATCH_SIZE")Precedence is explicit configuration, then environment, then the default. from_environment() builds the recipe. On weight-training recipes, service_config() forwards declared fields and shared artifact settings from the service configuration; override processor_config() when a processor needs renamed or derived keys. Never read deployment YAML from inside a processor.
Report #
from reef.core.reports import ReportBase, ReportValidationError, ScoredRolloutReportMethod-specific report contracts live in their method package; Reef does not import or re-export them.
A report type declares the feedback a method accepts, so malformed input fails at ingress with HTTP 400.
@dataclass(frozen=True)
class MyMethodReport(ReportBase):
score: float
task_id: str
rubric: str = ""
def validate(self) -> None:
if not self.task_id.strip():
raise ReportValidationError("metadata.task_id must be non-empty")Declare it through the recipe's report_type. score uses the top-level score channel; every other field uses metadata.<field>. Producers may attach extra fields the schema does not declare.
|
Annotation |
Accepted JSON |
Validation |
|---|---|---|
|
float |
number |
must be finite; ints normalize |
|
int |
number |
integral value; booleans rejected |
|
str |
string |
no coercion |
|
bool |
boolean |
no numeric substitutes |
|
Mapping[str, str] |
object |
every key and value a string |
Each supported type may also be written as T | None. Any other annotation, any other Union included, is a declaration error and raises TypeError when Reef first inspects the type. A field without a default is required; a field with one may be absent; JSON null is accepted only when the annotation permits it.
The same type serves both sides: a producer constructs it and calls to_dict(); a processor receives the parsed instance as context.parsed_report.
Processor #
from reef.train.processors import ComputedFeedbackProcessor, ReportedFeedbackProcessorA processor turns durable records into typed batches. Reef owns replay, retention, deduplication, pending batches, and exactly-once consumption; the method implements only the hooks below. They run synchronously on the trainer thread, so they must not block on network or model latency.
|
ReportedFeedbackProcessor |
ComputedFeedbackProcessor | |
|---|---|---|
|
signal arrives as |
a report referencing inference records |
information reconstructed from recorded traffic |
|
judgment |
synchronous, on available data |
async model or service call |
|
method owns |
eligibility, optional grouping, batch shaping |
correlation, slow judgment, sample and batch shaping |
|
Reef owns |
waiting index, retry dedup, grouping state, retention, replay |
worker lifecycle, queues, retention, replay |
Every processor gets the scenario's experiment logger as self.experiment_logger. Log finite numeric metrics under the processor namespace; processor code never imports W&B, and the logger is a no-op when tracking is off.
Reported feedback #
|
Hook |
Contract |
|---|---|
|
judge(context) -> ReportDecision |
return TRAIN, WAIT, or NEVER |
|
make_batch(units, batch_number) -> TrainingBatch |
shape accepted candidates |
|
decide_group(key, candidates) -> GroupDecision |
required only when judge() supplies a group_key |
|
output_schema |
the exact batch class returned |
|
exclusive_sources |
when true, a terminal report owns and releases its sources |
|
ordered_groups |
when true, ready groups batch by sortable group key |
Begin a score-based judge() with context.eligibility(): it returns WAIT while a referenced inference is missing, NEVER for permanently ineligible input, and None when the method should decide. Use ReportDecision.never(reason) for a rejection an operator should be able to diagnose. The processor logs each new reason and counts repeats.
ReportDecision.train(value) creates a singleton candidate. Supply group_key and an idempotent slot when the training unit is a complete comparison group; decide_group() then returns READY, INCOMPLETE, or DISCARD. Report-level WAIT and GroupDecision.INCOMPLETE differ: the first waits for a report's missing references, the second holds accepted candidates until their group fills.
Computed feedback #
Use this engine when later traffic completes an earlier record and judging it calls a model or another slow service.
|
Hook |
Contract |
|---|---|
|
ingest(record) |
correlate records; must not block |
|
async judge(job) |
slow judgment, on the processor's own worker |
|
make_sample(record, judgment) |
a PolicySample, or None to retire the record |
|
make_batch(samples, batch_number) |
the declared batch type |
|
expire(now) |
optionally return receipts whose completion window ended |
|
required_request_types |
optionally restrict which record types reach the processor |
Every ingest() starts with catch_up(now), then uses track(record), tracked_record(receipt), dispatch(job), retire(receipt), and abandon(receipt). A failed judgment or a None sample retires the record instead of failing a training step. All correlation state must be reconstructible from replay.
Batch #
from reef.train.types import (
GroupedPolicyBatch, PolicyBatch, PolicySample,
TraceBatch, TraceSample, TrainingBatch,
)A batch is a frozen dataclass with a stable batch_id. A processor returns the same pending batch until that id is acknowledged, so batch content must not depend on mutable external state.
TrainingBatch ├── PolicyBatch samples: tuple[PolicySample, ...] ├── GroupedPolicyBatch comparison_sets: tuple[tuple[PolicySample, ...], ...] └── TraceBatch samples: tuple[TraceSample, ...]
|
Type |
Typical use |
|---|---|
|
PolicyBatch |
singleton rollout and session-derived weight updates |
|
GroupedPolicyBatch |
group-relative objectives |
|
TraceBatch |
local harness-artifact evolution |
A PolicySample carries source_agent_record_id, tokens, loss_mask, rollout_log_probs, and reward. When available, it also carries runtime_load_id, action_mask, rollout_created_at, turn_count, topk_indices / topk_log_probs, runtime_load_spans, and extras, the field a processor uses for its own loss family. Use the shared assembly helpers in reef.train.processors.reported and reef.train.processors.common rather than re-parsing provider responses.
A TraceSample is not tokenized: it carries source_agent_record_id, the recorded payload unchanged, and the resolved score.
Subclass TrainingBatch only when the processor and backend genuinely share a different data contract; set output_schema to the new class and keep the batch serializable, with no handles to services, files, threads, or models.
Step preparer #
from reef.train.algos import StepSignalA preparer turns a reserved batch into a pure, backend-neutral signal. Normally it is a plain function named by the recipe as package.module:callable, and its module must be importable in both the service and the training process.
def prepare_step(batch: TrainingBatch, state: Mapping[str, Any]) -> StepSignal:
if not isinstance(batch, PolicyBatch):
raise TypeError(f"my_method requires PolicyBatch, got {type(batch).__name__}")
steps = next_steps(state)
return StepSignal(
action="train",
loss_family="my_method",
advantages=tuple(sample.reward for sample in batch.samples),
next_algorithm_state={"steps": steps},
metrics={"steps": steps},
)|
Field |
Contract |
|---|---|
|
action |
train runs a backend step; skip commits a state-only transition |
|
loss_family |
the backend objective for this step |
|
advantages |
optional per-sample values, in batch order |
|
next_algorithm_state |
committed only after the step succeeds |
|
metrics |
method telemetry carried to the commit record |
|
scheduling |
how a runtime materializes a grouped batch: unit is comparison_set or sample, batch_size is configured, actual, or a positive int |
A preparer owns method math and nothing else. It must not import a runtime, Ray, torch, or Slime; execute a training job; read deployment configuration; mutate the reserved batch; or commit state outside next_algorithm_state. The recipe's loss family, the signal's loss family, the driver environment, and the backend flags must agree. Reef rejects a mismatch at startup or during step preparation.
Use a StepPreparer subclass with @register_step_preparer only when several recipes need a stable shared name.
Harness method #
A harness-evolution method fills three slots. Reef runs the loop around them: snapshot, apply, run the paired episodes, record, publish or revert.
def propose(nodes, samples, models) -> Mutation | Sequence[Mutation] | None: ...
def evaluate(task, result) -> float: ...
class Selection: # optional
def decide(self, candidate, evaluation) -> SelectionDecision: ...|
Member |
Contract |
|---|---|
|
nodes |
the tree as (kind, config) pairs |
|
samples |
the batch of TraceSample records |
|
models |
the method's only path to a model: models.served is the model under test, models["teacher"] comes from evolution.models |
|
manifest |
optional, keyword-only: the previous step's FailureManifest, the per-task record of which episodes failed and how |
|
Mutation |
create, update, or remove on one root-level entry; a sequence applies as one composite proposal |
|
result |
one finished episode: exit code, stdout, stderr, and the parsed trajectory |
|
evaluation.metrics |
guarantees candidate_scores and current_scores: per-task score lists in task order, None for an episode that could not run |
Returning None from propose skips the step. The worked examples are in Evolve your harness.
Candidate evaluation #
from reef import CandidateEvaluationPlugin, EvaluationResult, SelectionDecisionA plugin measures a produced candidate before it is published, and decides. Reef enforces the fixed evaluate-then-decide order and verifies the decision kept the exact result it was given.
|
Member |
Contract |
|---|---|
|
evaluate(candidate) |
returns an EvaluationResult: evaluator name, version, and a metrics mapping |
|
decide(candidate, evaluation) |
returns a SelectionDecision whose outcome is select or reject, and which must carry the evaluation it was given |
A rejection leaves the previous artifact serving. An exception is fail-closed: Reef aborts the prepared candidate rather than publishing it. Make evaluations idempotent by candidate.candidate_id because recovery may repeat work whose result was not durably committed. The deployment names the factory in its evaluation section (Configuration).
Surface #
from reef.surface import (
Surface, create_harness_surface, create_skill_surface, create_weight_surface,
)A surface binds one frozen release to its consumers. WeightTrainingRecipe.build_surface() already calls create_weight_surface(), and CordisRecipe calls create_harness_surface(), so most methods never touch this.
Surface is a frozen dataclass whose capabilities are fields, not subclass identity. None means the capability is absent, and bare Surface() is the complete record-only configuration.
|
Field |
Type |
Contract |
|---|---|---|
|
loader |
ArtifactLoader | None |
recover the serving head, load rollback checkpoints |
|
inference |
InferenceHooks | None |
prepare provider requests, verify responses |
|
files |
FileTree | None |
back client pulls |
Two optional protocols extend those structurally, and the scenario checks for them with isinstance. ArtifactActivator adds loader.activate(artifact, runtime, source=...), making a version servable once it is final. LeasingInferenceHooks adds inference.begin_request(artifact, path), returning a lease the service releases when the attempt ends, so serving state such as a resident adapter stays protected for its duration.
A surface does not decide which records train, compute candidates, execute a training job, admit an artifact, or mutate the release chain. Artifact admission is separate, through Recipe.build_artifact_validator(). Native streaming behavior stays unchanged. A method should not add an HTTP proxy or copy Reef's record store.