Skip to content

Agent primitives

Seven base classes specialise Agent for the seven roles a node can play in an experiment. Each sets its kind — which must match the node's kind in workflow.json — and adds the one discipline that role needs.

graph LR
    Config[ConfigAgent<br/><i>config</i>] --> Data[DataAgent<br/><i>data</i>]
    Data --> Training[TrainingAgent<br/><i>training</i>]
    Training --> Algorithm[AlgorithmAgent<br/><i>algorithm</i>]
    Algorithm --> Evaluate[EvaluationAgent<br/><i>evaluation</i>]
    Evaluate --> Visualize[VisualizationAgent<br/><i>visualization</i>]
Primitive kind Adds
ConfigAgent config settings() — defaults merged with workflow params.
DataAgent data source_kind provenance label, check_frame() schema validation.
TrainingAgent training seed, split(), seed_everything().
AlgorithmAgent algorithm assumptions, assumption_summary().
EvaluationAgent evaluation Default metric ports and report(), which rejects NaN.
VisualizationAgent visualization figure_path(), save_figure().
CompositeAgent composite Executes a nested workflow.

You can always subclass Agent directly. The primitives exist because each enforces a rule that is easy to forget and expensive to get wrong.


ConfigAgent

Emits parameters and physical constants as a single settings object, so the values a run depended on are recorded in the trace instead of scattered through the code.

class KeplerConfig(ConfigAgent):
    ports: Ports = Ports(
        inputs=[],
        outputs=[Port("settings", "json", "Query parameters and physical constants")],
    )

    async def execute(self):
        settings = self.settings(
            min_period=0.1,
            max_period=100000.0,
            max_planets=5000,
            theoretical_exponent=1.5,
        )
        return {"settings": settings}

settings(**defaults) returns {**defaults, **self.params}, so a parameter sweep overrides one value from workflow.json without touching the code.

DataAgent

Ingests or generates the dataset, and says which it did:

class FetchExoplanets(DataAgent):
    source_kind: ClassVar[str] = "measured"   # or "simulated", "synthetic"

source_kind travels into the record, so a reader can tell at a glance whether a result rests on measurement or on simulation. check_frame(frame, columns, min_rows=1) validates a dataframe's schema up front rather than letting a missing column surface as a confusing error three nodes later.

TrainingAgent

The leakage guard. seed defaults to 0, and:

train, test = self.split(dataset, test_fraction=0.2, shuffle=True)

split() shuffles with random.Random(self.seed), so the same seed yields the same partition on every machine and every re-run. It raises rather than guess when test_fraction is out of range or the data has fewer than two rows. seed_everything() additionally seeds the standard library, NumPy and PyTorch.

Two representations compared on two different splits is not a comparison; a seeded split is what makes the difference attributable to the method.

AlgorithmAgent

The method under test. It asks for the approximations to be written down:

class FitKeplerLaw(AlgorithmAgent):
    assumptions: ClassVar[list[str]] = [
        "The log-log linear model is appropriate for this relationship.",
        "Host star mass variation introduces scatter but does not bias the slope.",
    ]

assumption_summary() renders them as Markdown for the report. An assumption that is never stated is one nobody can challenge.

EvaluationAgent

Every experiment needs exactly one measurement, and the framework needs to find it. EvaluationAgent therefore ships default output ports:

Port Type Required
metric str yes
value float yes
detail json no
return self.report(
    metric="r_squared",
    value=fit["r_squared"],
    detail={"recovered_exponent": exponent, "n_planets": fit["n_planets"]},
)

report() refuses a non-numeric value and refuses NaN outright:

AgentError: evaluate: metric 'rmse' evaluated to NaN. A NaN result is a
failed computation, not a score.

The result_measured and metric_is_finite gates read the value output of an evaluation node. A workflow without an evaluation node cannot be scored at all.

The evaluation node must not know the threshold

target_value lives in GOAL.md and is compared outside the workflow. Keep it out of the node.

VisualizationAgent

Figures belong in the run directory, next to the trace that produced them:

path = self.save_figure(fig, "residuals", dpi=150)   # returns a run-relative path

figure_path(name, extension="png") reserves the path; save_figure(figure, name, dpi=150) writes a matplotlib figure and records it in provenance.

CompositeAgent

A node whose implementation is an entire nested workflow, so a validated sub-pipeline can be reused as one step. Declare kind: "composite" and point the node's workflow field at the nested definition; the composite's terminal outputs surface as its own output ports.

Choosing

If the node… Use
emits constants or hyperparameters ConfigAgent
loads, downloads or simulates data DataAgent
fits a model or needs a train/test split TrainingAgent
implements the thing being tested AlgorithmAgent
computes the target metric EvaluationAgent
renders a figure VisualizationAgent
wraps a whole sub-experiment CompositeAgent
does none of these Agent

Reference

science_adk.primitives