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:
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:
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 |