Skip to content

agent-authoring

When this skill applies

Writing an agent

One node, one file, one class, one method.

"""Fit the oscillation period from a trajectory."""

from typing import ClassVar

from science_adk import AlgorithmAgent, Port, Ports


class FitPeriod(AlgorithmAgent):
    """Recover the period from zero crossings of the angle."""

    assumptions: ClassVar[list[str]] = [
        "The motion is undamped, so every cycle has equal duration.",
    ]

    ports: Ports = Ports(
        inputs=[Port("trajectory", "dataframe")],
        outputs=[Port("period", "float")],
    )

    async def execute(self):
        frame = await self.input("trajectory")
        await self.log(f"analysing {len(frame)} samples")
        ...
        return {"period": period}

The file lives at agents/<node-id>.py, matching the node's id in workflow.json.

The three rules the engine enforces

Ports must match the DAG exactly. Same names, both directions. If they disagree, validate will say so and nothing runs.

Every class attribute needs a type annotation. Agents are pydantic models, so a bare GRAVITY = 9.81 fails at import with a confusing message about model fields. Write GRAVITY: ClassVar[float] = 9.81. Better still, put it in params where the optimizer can reach it.

execute returns exactly the declared outputs. No extras, no omissions, no None for a required port.

What you get

await self.input("name")            # a declared input, hydrated
self.param("name", default)         # from `params` in workflow.json
await self.call_tool("name", **kw)  # a configured tool
await self.fetch(url)               # an HTTP GET, with backoff
self.artifact("plot.png")           # a path inside this run's data/
await self.log("...")               # progress, into the trace

Inputs, tool calls and fetches are recorded in a provenance ledger the agent cannot write to. That ledger is what makes the audit meaningful, and it is why tools in workflow.json has to be true.

Returning data

Return ordinary Python values. Anything large — a DataFrame, an ndarray, a long list — is written to data/ automatically and travels onward as a pointer, so the trace stays readable and the file stays with the run. Downstream nodes receive the real object, not the pointer.

Parquet, npy and json are the storage formats. There is no pickle path: an artefact only readable by the process that wrote it is not an artefact.

Do not

Do not catch exceptions to keep going. A crash is information. A swallowed crash becomes a fabricated number the moment something downstream uses it. The validator rejects except: pass.

Do not hard-code a result. If it should be computed, compute it.

Do not read the target. The goal's threshold is deliberately invisible to agent code. An evaluation node that knows the number it must beat is one that will eventually be written to beat it.

Do not call a language model. An agent is a reproducible computation. The reasoning happened when you wrote the code.

Primitives

Pick the base class matching the node's kind:

  • ConfigAgentself.settings(**defaults) merges declared defaults with params.
  • DataAgent — set source_kind to "measured", "simulated" or "synthetic", truthfully; check_frame(df, [...]) asserts shape early.
  • AlgorithmAgent — populate assumptions.
  • TrainingAgentself.split(data) and self.seed_everything(). Never evaluate on the training split.
  • EvaluationAgent — return self.report(metric=..., value=...). It rejects NaN, because a NaN is a failed computation rather than a poor score.
  • VisualizationAgentself.save_figure(fig, "name").
  • CompositeAgent — set workflow to a nested workflow.json.

Dependencies

Anything installed in the environment can be imported. numpy, pandas and matplotlib are common; the engine itself does not require them. If a run fails on a missing import, install it rather than working around it.

Check before running

science-adk validate          # ports, structure, integrity patterns
science-adk run               # execute
science-adk run --only fit    # just this node and its dependents
science-adk run --reuse       # skip nodes whose inputs and code are unchanged

--reuse is what makes iteration bearable: change one node and only that node and its descendants re-execute.

When a node fails

Read the traceback in trace.json and load the debugging skill. Do not weaken the node until it passes.