Skip to content

The contract

The contract defines what an agent receives and what it returns. It is written in LinkML in sdk/src/dd_sdk/schema/data_director.yaml, which the workbench and every agent share through the SDK. The JSON Schema and SHACL shapes in generated/ beside it are produced from that file and are never edited by hand.

How the contract is written

LinkML is a data modelling language written in YAML. A LinkML schema declares classes, their slots (fields) and enumerations. Generators then produce other forms from that one source. Each slot can also name the term it means in an existing vocabulary. For example, slot_uri: dcterms:title says that a dataset's title is a Dublin Core title, so a record can be read as linked data without a separate mapping.

The contract exists in three forms:

Form Where Role
LinkML source sdk/src/dd_sdk/schema/data_director.yaml The definition. Read it to learn what a slot means.
JSON Schema and SHACL sdk/src/dd_sdk/schema/generated/ Generated by uv run dd-gen-schema (dd_sdk/schema/gen.py). At runtime, documents are validated against the JSON Schema. Agents in other languages use the JSON Schema too.
Pydantic models sdk/src/dd_sdk/contract/models.py Written by hand to mirror the source. Python code uses them to build and read documents.

LinkML is needed only to regenerate the schemas. It is not a runtime dependency. Some rules cannot be stated in JSON Schema, such as "a succeeded outcome carries a payload". The LinkML source states these rules in prose and dd_sdk/contract/validate.py enforces them. Contract records why LinkML was chosen and what else was considered.

To change the contract:

  1. Edit the LinkML source.
  2. Run uv run dd-gen-schema and commit the regenerated files.
  3. Update the Pydantic mirror in models.py.
  4. Run uv run pytest sdk. The tests check that the generated files match a fresh generation and that every model validates against the schema.

A change to a fixed format, such as a slot, a required field or the outcome vocabulary, changes a spec (Contract).

The request

An agent receives an InvocationRequest. Its main fields are:

Field Meaning
invocation_id A UUIDv7 identifier for this run. UUIDv7 values sort by time (Contract).
agent_id The agent to run, for example advisor.quality.
input The input document.
conversation_id Optional. The conversation this run is a turn of (see below).
parent_invocation_id Set only by the conductor, on a run another agent delegated. A caller that sets it is refused.

A request does not name an institutional profile. The deployment chooses one, and a request that carries policy_bundle_ref fails validation (Policy and identity).

The input is one of several input classes: DatasetProfile, MetadataRecord, Claim, Salutation, Message, FileInventory or DraftRequest. Its schema_class field says which one it is (Contract).

Three input classes belong to the core. Message is described under conversations, below. FileInventory is what a project holds in place of its data files (Projects). For each file it gives the path relative to the dataset's root folder, the size, the SHA-256 digest, the media type and every PRONOM identifier the file matched, with how they were found. For a table it also gives the fields as a Frictionless Table Schema (TableField). It is made by dd-inventory, a command run where the data live (inventory/), and it never holds a file's content.

DraftRequest is what every drafter accepts: an agent that returns a Document is asked for one document_kind and given the project's facts. These are the inventory, the requirements of the repository chosen (RepositoryRequirements, the merged set of Deposit) and, to regenerate a document, its current_version. A CurrentVersion gives the version as a DocumentVersionRef, its text and media type, its version_state (proposed, approved or rejected) and the decision_reason a person gave, and the drafter names that version in its payload's revises. Because the class is the core's, a drafter can be replaced without its caller changing. The conductor is to fill the facts from the project, TODO(#394). Until it does, the caller sends them.

The envelope

An agent's response is wrapped in an Envelope. Every envelope contains:

Field Meaning
outcome What happened. Its status is one of the five values below.
payload The agent's result. Present only when the status is succeeded.
evidence The source records the result rests on. Each item carries a content hash and names the canonicalisation used to compute it (Evidence and grounding).
grounding_mode The grounding mode the agent declared (grounding.md).
policy_bundle_ref, policy_digest The institutional profile the conductor applied, as profile:<id>@v<version>, and the SHA-256 of its file (Policy and identity).
telemetry The trace id, the model id, token counts, and energy fields. The energy fields are always empty for now (not_measured).
requires_human_review Always true. The schema fixes it as a constant, so no envelope can claim otherwise.
problem Details of the problem, when the status is failed or suspended.
conversation_id, parent_invocation_id Copied from the request, when present.
delegations For an agent in grounding mode delegation: each run it delegated, with that run's agent, version, status and envelope hash. Omitted when empty.

The envelope also carries the invocation id, the agent id and version, and the completion time. The conductor fills these in, not the agent.

Outcomes

The Blueprint does not define a set of outcomes. This vocabulary is the workbench's own proposal.

Status Meaning Also carries
succeeded The agent produced a result it stands behind. A payload.
abstained The agent declined to give a result. This is a valid answer, not an error. A reason_code, such as no_candidates_retrieved.
referred The agent passed the decision to a named person or role. A reason_code and referred_to.
failed The agent or the workbench could not complete the run. A problem in RFC 9457 Problem Details format.
suspended The run is waiting for something outside the workbench. A problem that says what must happen before it can resume.

Payloads

Each payload class is specific to an agent: Recommendations, QualityReview, Greeting or Reply. Every payload class includes the Grounded mixin, which adds a grounded_on list. Each entry in that list names a source by its identifier and content hash. This is the only place a payload may assert which sources it relies on. The grounding linter checks each entry.

Two payload classes belong to the core, because the harness and the app read them without knowing the agent: Reply, below, and Document, a project document such as a metadata record, a README or a data dictionary. A Document gives its document_kind, its media_type and its document_text, and names the version it revises when an agent regenerates it (Projects).

Conversations

A conversation is a sequence of runs that share a conversation_id. Each turn is one ordinary run with its own envelope, so every check applies to it (Orchestration).

A conversational agent accepts Message: the text of this turn (message_text) and the conversation so far (history, a list of ConversationTurn). Each agent turn in the history names the agent, version and run it came from. The history is part of the input, so the input hash covers everything the agent was shown. The agent replies with a Reply payload (reply_text, reply_derivation).

The workbench keeps no separate conversation store. GET /conversations lists conversations and GET /conversations/{id} returns one, read back from the stored runs: each turn with the runs it delegated, the stored inputs, where an agent's version changed between turns, and the history to send with the next Message.

An orchestrator hands work to other agents only through the workbench. Each hand-off is a run of its own, stored with parent_invocation_id, and listed in the parent envelope's delegations. conductor.md describes how.