Agents¶
Each directory here is one agent: its own package, its own tests, and its own service. The
workbench does not import any of them. It reaches each agent over A2A at the URL listed in
workbench/agents.yaml (Agents).
An agent is an AgentSpec plus one method, run(request, ctx) -> AgentResult
(dd_sdk.agent). dd_sdk.serve puts it behind A2A: it publishes the agent card with the spec,
runs the agent under a tracer parented to the workbench's span, and returns the result with the
spans the agent recorded. The workbench's conductor then holds the agent to its spec:
- it refuses an input of a class the agent did not declare, or one its class schema rejects;
- it rejects a payload of a class other than the one declared, or one its class schema rejects;
- it records the declared grounding mode on the envelope;
- it runs the grounding linter's rules for that mode over the returned spans.
An agent never sets identifiers, timestamps, telemetry or its grounding mode.
An agent owns its input and payload classes
(Contract). It declares
them in a LinkML file of its own, which imports the core contract, and its card carries each
class's JSON Schema pinned by a digest. The workbench checks inputs and payloads against those
schemas, so it needs no copy of an agent's classes, and adding one needs no SDK release. What
every agent shares is the core, sdk/src/dd_sdk/schema/data_director.yaml: the request, the
envelope, outcomes, grounding, evidence, delegation, the principal, and the chat classes
Message and Reply. Its version is the contract version each card declares; the workbench
lists an agent built against an incompatible one as incompatible.
| Package | Agent id | Accepts | Mode | Payload | Serve | Status |
|---|---|---|---|---|---|---|
advisor-quality/ |
advisor.quality |
MetadataRecord |
input_only |
QualityReview |
dd-advisor-quality, port 8102 |
Demonstration: weighted completeness from checks.yaml. |
example-hello/ |
example.hello |
Salutation |
none |
Greeting |
dd-example-hello, port 8101 |
Template. Greets whoever the input names; the worked example of every step below. |
orchestrator-stub/ |
orchestrator.stub |
Message |
delegation |
Reply |
dd-orchestrator-stub, port 8106 |
Rule-based stand-in for the orchestrator: routes each message by routing.yaml and delegates through the workbench (Orchestration), or lists the steps from the researcher's Blueprint entry point (Orchestration). |
orchestrator-model/ |
orchestrator.model |
Message |
delegation |
Reply |
dd-orchestrator-model, port 8107 |
The model-backed orchestrator (#21): Claude decides which agents to ask, from the manifest the harness publishes, and writes the reply. It has no tools except those delegations: no MCP server, web search or Skills (Orchestration). Needs a model key (orchestrator-model/README.md). |
advisor-standards/ |
advisor.standards |
DatasetProfile |
retrieval |
Recommendations |
dd-advisor-standards, port 8105 |
Recommends vocabularies, ontologies and formats from FAIRsharing (committed snapshot, or live with the snapshot as fallback). Configured by DD_STANDARDS_* (advisor-standards/src/dd_agent_advisor_standards/README.md). |
advisor-policy/ |
advisor.policy |
PolicyContext |
retrieval |
PolicyChecks |
dd-advisor-policy, port 8108 |
Alpha. Phase 2: checks what funder, journal and institutional data policies ask, from FAIRsharing's policy records, and flags where they differ; also names any repository they require for Phase 3 (advisor-policy/README.md). |
advisor-repository/ |
advisor.repository |
RepositoryNeeds |
retrieval |
RepositoryOptions |
dd-advisor-repository, port 8109 |
Alpha. Phase 3: recommends repositories from re3data, or describes the mandated ones, with PIDs, access, costs and certification as re3data and FAIRsharing record them (advisor-repository/README.md). |
advisor-ethics/ |
advisor.ethics |
SensitivityContext |
none |
SensitivityFlags |
dd-advisor-ethics, port 8111 |
Mock. Flags field names that suggest sensitive data and asks what must be confirmed about consent and embargo; never decides how the data are treated. |
advisor-files/ |
advisor.files |
FileInventory |
none |
FormatAssessment |
dd-advisor-files, port 8110 |
Mock. Lists each file of a project's inventory with the format its media type or extension suggests and whether that format is open, without reading any file. |
advisor-dmp/ |
advisor.dmp |
DmpCommitments |
none |
DmpDiscrepancies |
dd-advisor-dmp, port 8112 |
Mock. Compares what a data management plan committed to share with what is planned at publication, and flags each difference for human review without enforcing it. |
advisor-fairness/ |
advisor.fairness |
PublishedRecord |
none |
FairnessReport |
dd-advisor-fairness, port 8117 |
Mock. Checks a published dataset's metadata against a short list of FAIR-related presence checks and suggests what to retrofit, without retrieving anything or giving a FAIRness evaluation. |
advisor-metadata/ |
advisor.metadata |
DraftRequest |
none |
Document |
dd-advisor-metadata, port 8113 |
Mock. The stub drafter of the metadata record (#200): a DataCite record with every property the chosen repository asks for, filling only the resource type and the formats and sizes the inventory gives. |
advisor-documentation/ |
advisor.documentation |
DraftRequest |
none |
Document |
dd-advisor-documentation, port 8114 |
Mock. The stub drafter of the README and the data dictionary (#200): copies the inventory's files and fields, and marks the description, methods, definitions, units, licence and citation for the researcher. |
advisor-rights/ |
advisor.rights |
RightsContext |
none |
RightsOptions |
dd-advisor-rights, port 8115 |
Mock. Lists data licences that fit the reuse wanted, and access levels with when each applies, for the researcher to choose from; never chooses. |
advisor-linking/ |
advisor.linking |
LinkCandidates |
none |
RelatedIdentifiers |
dd-advisor-linking, port 8116 |
Mock. Recognises DOIs, ORCID iDs, ROR IDs and grant numbers by form, checks ORCID checksums, gives canonical https forms and suggests how to record each link, resolving nothing. |
Start every agent with scripts/run-agents.sh from the repository root, or one agent with
uv run dd-example-hello serve --port 8101. The script restarts an agent when a file in its package
changes, its system_prompt.md included, unless scripts/dev.py start is given --no-reload;
serve --reload does the same for one agent.
An agent id is <group>.<name>. The groups are orchestrator (agents a person converses with,
which delegate to others), advisor (agents that answer one kind of question, each a persona from
the roster in Agents with a title that people see) and
example (templates). A directory, package and serve command spell the id with hyphens or underscores:
advisor.policy is advisor-policy/, dd_agent_advisor_policy and dd-advisor-policy.
Start from example-hello/¶
example-hello/src/dd_agent_example_hello/agent.py is the smallest agent that walks the whole of the recipe
below with no domain logic in the way. It has:
- its own input class and its own payload class;
- a declared grounding mode;
- its payload fields' derivations;
- a console script;
- a profile entry;
- a sample;
- marked tests.
It is commented against the numbered steps. Copy the directory, rename it, and delete the greeting.
It is also the check on the claim in the last line of this file. example.hello has no reason to
fail other than the harness. If adding an agent ever needs an edit to the conductor, linter,
CLI, transports or viewer, this agent will show it.
Adding an agent¶
- Declare what it reads and returns. A conversational agent can use the core's
MessageandReply. Any other class belongs to the agent.example-hello/is the worked example: src/dd_agent_<name>/schema/<name>.yamldeclares the classes in LinkML. It importslinkml:typesanddata_director(the core), and has its ownidand prefix. An input class carries the slotschema_class; a payload class carriesschema_classandmixins: [Grounded].uv run dd-gen-schema agents/<name>/src/dd_agent_<name>/schema/<name>.yamlwrites one JSON Schema per class togenerated/beside it. Commit them; never edit them by hand.src/dd_agent_<name>/classes.pyholds a Pydantic model per class, subclassingdd_sdk.contract.models.Frozen(input) orGrounded(payload), withschema_class: Literal["<Class>"].- The spec names each class as
ClassSchema.of(Model). It reads the generated schema from the model's package and refuses a model whose fields differ from it.
A test calls dd_sdk.schema.gen.stale(<path to the LinkML file>) and expects [], so a
stale generated schema fails the agent's own suite. Two agents may define classes of the same
name; the digest, not the name, identifies a class in a stored run.
Say which steps of the Blueprint's process flow the agent helps with, by their ids in
dd_sdk.process_flow: serves=("setup.vocabularies", "preparation.formats"). The
orchestrators name it at those steps and no others (Orchestration). Name steps, not
requirements: the requirements it bears on follow from the steps, and whether it meets them is
for the requirements register.
2. Decide its grounding mode (Evidence and grounding).
- retrieval if it consults external records. Emit one
dd_sdk.tracing.retrieval_span(ctx.tracer, source_id, content_hash) per record consulted,
before any model call. Put a GroundingRef for each record the payload rests on in
grounded_on.
- input_only if it works over the input and may call a model.
- none if it is deterministic.
- delegation if it answers by handing work to other agents (an orchestrator). Call
ctx.delegate(agent_id, input), never another agent directly; the workbench runs the child
and returns a Delegated whose ref and evidence are what the payload cites for it
(Orchestration). ctx.delegate is None when no grant was issued (for example under
workbench invoke); handle that as an outcome. orchestrator-stub/ is the worked example.
In input_only and none, grounded_on and evidence cite ctx.input_ref /
ctx.input_hash and nothing else. Always use ctx.tracer. A span started some other way is
outside the invocation's trace, and the linter's G0 rule withholds the output.
3. Hash what you retrieved with a registered canonicalisation from dd_sdk.evidence
(Evidence and grounding). Register a new name there if none fits; never invent one in the agent.
4. Write the package. Copy example-hello/. You need:
- pyproject.toml depending on data-director-sdk, with a console script
dd-<name> = "dd_agent_<name>.agent:main";
- a class with spec = AgentSpec(...) and run;
- a module-level build() -> Agent and main() -> int that returns serve.main(build).
Read the agent's own DD_<AGENT>_* environment variables in build. A content problem is an
outcome (abstained with a reason code), not an exception.
An agent that calls a model keeps its system prompt in system_prompt.md beside the module
that sends it, and reads it with Path(__file__).with_name("system_prompt.md"), so the prompt
can be read and reviewed as prose. CI treats that file as the agent's code, not as prose.
orchestrator-model/ and advisor-standards/ are the examples.
An agent that is exploratory — not yet validated, likely to change shape — ships with a
pre-release version (version = "0.1.0a1", PEP 440) instead of the usual 0.1.0, and its
README.md and its row in the table above say plainly that it is alpha. There is no separate
prototype tree for this: an experimental agent is a normal package under agents/, just marked
by its version. Bump it to an ordinary 0.1.0-style version once it is no longer experimental.
5. Add it to the workspace. Add the distribution to the dependencies and
[tool.uv.sources] of the root pyproject.toml, then run uv sync --all-packages. A package
that lives outside this repository skips this step; it only has to be served.
6. Register it with the workbench. Add one entry to workbench/agents.yaml with a name and
the URL it is served at, and its port to scripts/run-agents.sh.
7. Enable it in the profiles that should run it (workbench/profiles/default.yaml,
workbench/profiles/test-permissive.yaml). Registration is not permission.
8. Ship a sample input and declare the payload's derivations.
- Put a sample input, with schema_class set, in workbench/samples/. The viewer builds its
input form from the class schema in the agent's card.
- If the agent has a payload, declare in spec.derivations how it produces each field that
it does not copy from input or evidence, for example
{"rationale": Derived(Derivation.MODEL, recorded_in="rationale_derivation")}. A field a
model may write names the sibling that records, per value, how it actually came about. The
spec refuses a path or a sibling that the payload class does not have. Ship no presentation:
the viewer lays the payload out from its schema, in LinkML slot order (Agents).
9. Test it in <name>/tests/, with a test module name no other package uses. Test the
agent through a conductor with dd_harness.testing.make_conductor(runs_dir, YourAgent()),
which serves it in memory over A2A exactly as the workbench calls it. Mark each test with the
requirement identifiers it actually exercises (docs/requirements.yaml). Do not
claim a Blueprint R a demonstration does not meet.
10. Optionally, evaluate it (Evaluation and conformance). Put an Inspect AI task in <package>/evals/, built
on workbench.evaluation.invoke_agent and its generic scorers, with the agent's own cases
and scorers beside it and a committed baseline.json. dd_agent_advisor_standards.evals is the worked
example.
Nothing in the SDK, conductor, linter, CLI, transports or viewer is edited.