Skip to content

Agents

CI Coverage

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

  1. Declare what it reads and returns. A conversational agent can use the core's Message and Reply. Any other class belongs to the agent. example-hello/ is the worked example:
  2. src/dd_agent_<name>/schema/<name>.yaml declares the classes in LinkML. It imports linkml:types and data_director (the core), and has its own id and prefix. An input class carries the slot schema_class; a payload class carries schema_class and mixins: [Grounded].
  3. uv run dd-gen-schema agents/<name>/src/dd_agent_<name>/schema/<name>.yaml writes one JSON Schema per class to generated/ beside it. Commit them; never edit them by hand.
  4. src/dd_agent_<name>/classes.py holds a Pydantic model per class, subclassing dd_sdk.contract.models.Frozen (input) or Grounded (payload), with schema_class: Literal["<Class>"].
  5. 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.