ARIADNE
agent context kit

Two agents, one graph, opposite directions

Both are built on the DataHub Agent Context Kit and can run over either transport. Neither has a language model in its decision path, and that is a choice rather than an omission: the answers are traversals and measurements, so they are reproducible, and a person who disagrees with one can check it rather than argue with it.

before the change

Blast radius

Root cause is the glamorous half and the wrong half to reach for first. By the time anyone runs it the model is already serving decisions off the thing nobody meant to add. The cheap moment is the pull request, when the change is still a diff and the only cost of knowing is a comment.

python tools/blast.py analytics_marts.dim_person --column public_coverage_flag --policy eu_ai_act
== blast radius of a change to analytics_marts.dim_person.public_coverage_flag ==
   read through kit, under Regulation (EU) 2024/1689, Annex III high risk systems

  examine      in scope because it carries personal_data, and EU AI Act watches that tag.
               under Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV

  reaches 1 deployed model
    workforce-classifier_3     mlflow_production, 5 hops

  and 1 registered but not serving
    workforce-classifier_1     mlflow_archived

  through 2 downstream tables
    1 hop  [dbt] workforce_features carrying public_coverage_flag
    2 hop  [postgres] workforce_features carrying public_coverage_flag

  and 8 training runs that consumed it

The split between deployed and registered but not serving is the part that makes this readable. A change reaching forty registered models and no live one is a different change from one reaching a single live model, and a plain downstream count cannot tell them apart.

The same answer, shaped for a pull request

python tools/blast.py analytics_marts.dim_person --column public_coverage_flag --policy eu_ai_act --comment
**Blast radius: `analytics_marts.dim_person.public_coverage_flag`**

> **examine**: in scope because it carries personal_data, and EU AI Act
> watches that tag. Under Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV.

Reaches **1 deployed model**:

| model | stage | hops |
| --- | --- | --- |
| `workforce-classifier_3` | mlflow_production | 5 |

<details><summary>2 downstream tables</summary>
...
</details>

<sub>Ariadne, from DataHub lineage, under Regulation (EU) 2024/1689.</sub>

It exits non zero only when a prohibited column reaches something that is serving. A change that merely touches a live model is a thing a person should read, not a build failure, and a check that blocks on everything gets switched off within a week.

after the change

Root cause

Every ML observability tool can tell you a model changed. That is the easy half and it is where they stop, because a distribution plot has no way to reach the pull request that caused it. Ariadne holds both ends: the measurement knows the number moved and by how much against its own noise, and the graph knows which columns the deployed model gained between those two moments and where each came from.

python tools/rootcause.py --model workforce-classifier --policy eu_ai_act --via mcp
== why did workforce-classifier move ==
   read through mcp, under Regulation (EU) 2024/1689, Annex III high risk systems
   comparing version 2 (2026-07-27T13:30:31) with version 3 (2026-07-28T08:34:06)

  accuracy went up, 0.6955 to 0.7310 (+0.0355)
  so nothing that watches model quality had a reason to object

  1 measurement moved beyond noise
    disability (With a disability against Without a disability)  0.7489 to 0.7883  +0.0394, 12x the noise

  the deployed model gained 1 column between those two moments

    public_coverage_flag
      examine, in scope by tag, Article 10(2)(f), Article 10(2)(g), Article 9, Annex IV
      enters at postgres:raw_person, 7 hops back
      called pubcov where it enters, so a search by name would not have found it
      defined in models/marts/dim_person.sql
      defined in models/marts/workforce_features.sql
      defined in models/staging/sources.yml
      defined in models/staging/stg_person.sql

  cause: public_coverage_flag

It starts from the measurement, not from the diff. Plenty of columns get added that change nothing, and reporting them all would be a changelog rather than a cause. And it will not rank causes by plausibility when it cannot tell them apart: if two columns arrived together it names both and says the graph cannot choose. A confident single answer would read better and would be invented.

two mistakes worth keeping

Both agents were wrong first, in the same way

Both are written into the skill, so an agent following it gets the answer rather than the plausible version of it.

analytics agent

The pattern taken from DataHub's own agent

DataHub's analytics-agent is a full application with a backend, a frontend and a helm chart, and embedding it here would have been theatre. Its own docs call it a reference implementation, so the useful thing to take was the shape rather than the code: a context platform abstraction in front of each way of reaching a catalog, built by a factory rather than chosen at the call site.

The part worth copying most was the smallest. It discovers what tools a platform offers instead of assuming, so asking for something a deployment does not have fails at the point of asking with the real list. Against this instance the MCP server advertises six tools and keeps three groups switched off, so the difference is not hypothetical.

analytics-agentariadnesame idea
ContextPlatformContextone interface, many transports
MCPContextPlatformMCPContextstdio child process
native_datahubKitContextin process SDK
build_platform()open_context()factory, not a call site choice
tools via tools/listtools via tools/listdiscovered, never hardcoded
the judgment these agents apply, written down