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:
- Edit the LinkML source.
- Run
uv run dd-gen-schemaand commit the regenerated files. - Update the Pydantic mirror in
models.py. - 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.