Skip to content
mcpd
no daemon
Source

Aspect notes

An aspect is an axis. An aspect note is one sentence recorded against one finished run along that axis. Your process writes most of them, with your model and your key, the daemon writes the outcome notes, and mcpd stores each one as a signal.

An axis, and a sentence recorded against it

An aspect is an axis a run can be described along: what the user wanted, how the interaction ended. An aspect note is one sentence recorded against one trace along one of those axes. The two are different kinds of thing and they are stored apart. An aspect is a definition, and a note is a recording made against it.

Keeping the two nouns straight is worth the effort, because every query parameter in this family names one or the other. An aspect carries a name, a display label, one line of description and, where a model writes the sentence, the instruction handed to whatever produces it. A note carries the sentence, the trace it is about, the version of the definition it was written against, and the model that wrote it.

A note is not a score. A score is a number on a scale it declares, and the label on a score is a categorical result from a fixed set. A note is prose, and nothing here ever compares two notes by value. When what you want is a number you can chart and alert on, record a score instead: see Scores and feedback.

Who writes one, and with whose model

mcpd stores the instruction for an aspect and hands it out on request. It does not read it, and it does not act on it. Whatever fetches the instruction, calls a model with it and posts the sentence back is yours: your endpoint, your model, your credential, your rate limit and your bill. The daemon makes no model call of its own along the way.

That keeps the rule the rest of mcpd holds to. mcpd is not in your agent's request path to a model, and writing a note does not put it there either. The call that produces a note happens after the run has finished, against a trace that is already stored, from a process you started. Delete that process and the runs still record; only the sentences stop.

A note arrives as a signal, not through a side door. It goes in the aspect_notes array of a batch posted to /v1/ingest with a key scoped to ingest, beside the spans, logs, metrics, scores and feedback for the same trace, under the same idempotency key. A batch replayed after a timeout is a no-op that still returns 200, so a worker that cannot tell whether its last post landed can send it again.

Ingest pipeline
  1. Decode

    OTLP or native, gzip or plain

  2. Sample

    Decided once, at the root

  3. Redact

    Before anything is written

  4. Price

    From the configured price book

  5. Store

    One transaction per batch

Redaction is stage three, not a cleanup job. A value that a rule matches never reaches storage at all.

A note travels those stages like everything else, and the third one is the one that matters for prose. A model writing about a run repeats what it read, so an address or a card number can reach a sentence that nothing in your code composed. The redactor takes it out on the way in, before the note is written down. There is no cleanup pass afterwards, because there is nothing left to clean up.

recording a note
$ curl -s "$MCPD/v1/api/aspects?project=support""aspects": [  { "name": "goal",      "derived": false, "enabled": true  },  { "name": "sentiment", "derived": false, "enabled": false },  { "name": "behaviour", "derived": false, "enabled": true  },  { "name": "outcome",   "derived": true,  "enabled": true  }],"limits": { "min_notes_to_cluster": 50, "max_notes_per_pass": 20000 }$ # your worker reads the instruction, calls your model, posts the sentence$ curl -s "$MCPD/v1/ingest" -H "Authorization: Bearer $INGEST_KEY" -d @batch.json{ "accepted": 1, "rejected": 0, "duplicate": 0 }$ curl -s "$MCPD/v1/api/traces/$TRACE/aspects""notes": [ { "aspect": "goal", "definition_version": 3,             "model": "gpt-4o-mini",             "text": "The customer wanted a refund on a late order." } ]

The four aspects that ship

Four aspects ship with the daemon, compiled into it. There is one list that says what exists, and no client holds a second one that could disagree with it: GET /v1/api/aspects serves that list with the bounds a clustering pass runs under, and every screen reads that route.

AspectThe axisWho writes the sentence
goal What the user wanted. Your process, with your model.
sentiment The tone they expressed. Your process, with your model.
behaviour What the agent did about it. Your process, with your model.
outcome How the interaction ended. The daemon, with no model at all.

Those four are the whole catalogue in this build. You cannot add an aspect, edit one, or switch one on for a single project. The route also publishes max_aspects, the ceiling on how many aspects one project may have, and it is fixed at 32.

The outcome aspect is derived, and needs no model

outcome ships with derived set. The daemon writes its notes itself, from four things it already holds: the run's status, the error class of the step that failed, the silent_producer flag on the cause report, and the divergence watch. No model is called, no endpoint is contacted and no key is needed. It writes them during the analysis pass, so only when aspects.enabled is set in the config file.

That is there to solve a specific problem with this feature, which is that the interesting screen is the last thing you reach. Wiring a model, writing an instruction and running a worker long enough to clear the floor is a morning's work before you have seen the thing you are building towards. A deployment that sets aspects.enabled and configures nothing else still fills one aspect from the runs it already has, so the pipeline can be watched working end to end before anyone spends a token on it.

A derived note carries no model, because nothing wrote it but the daemon, and its aspect carries no instruction, because there is nothing to instruct. Those two absences are how you tell a derived note from one of yours without reading the catalogue.

Switching an aspect off

Whether an aspect is on is deployment configuration, not a setting per project. The aspects block of the config file holds a disabled list of aspect names. An aspect named there is off on every project, and every other aspect is on. Every catalogue row carries the resulting enabled flag, and no endpoint changes it: the read API reports the state and does not set it.

Switching an aspect off changes what the pass does afterwards and never rewrites what is stored. No pass clusters it, its progress no longer counts towards readiness, and for outcome the daemon stops writing notes. Notes already stored stay readable, and the aspect stays in the catalogue. That is why the catalogue returns disabled rows as well as enabled ones. A name that vanished from the list would make every note filed under it look like a mistake.

An aspect can be switched off. It cannot be deleted, for the same reason: notes already name it, and a note whose aspect no longer exists is a sentence filed under nothing.

Every note names a definition version

Every note records the definition_version it was written against, and the catalogue reports each aspect's version. In this build every aspect is at version 1 and none can be edited, so the outcome notes the daemon writes all carry 1. A note your own process posts carries the version it sends.

Read the version when a set of notes surprises you. Two notes along one aspect that answer visibly different questions can come from two writers that sent different versions, and that is the first thing to rule out.

Reading what was written about one run

GET /v1/api/traces/{trace_id}/aspects returns every note recorded about one trace, in catalogue order, at most one per aspect. It is not entity scoped, because a trace id is unique across the deployment.

Read it beside the trace itself. This is the shortest route from an aggregate back to a run: a note is the sentence that put this trace in each of its themes, so it is also the place to check a grouping you do not believe. An empty list is the ordinary answer for a recent trace. Writing a note lags ingestion, sometimes by a long way.

What a note proves, and what it does not

A note proves one thing: that this sentence was recorded against that trace, by the definition named in aspect, at the version in definition_version. It does not prove the sentence is true.

Nothing checks a note against the trace it describes. mcpd cannot: it did not make the model call, it has no view of what the model was shown, and it has no second opinion to compare the sentence against. So a note is evidence of what a definition said about a run, and the run is what settles whether the definition was right. When the two disagree, the trace wins. Read a note as a pointer into the recording, never as a finding about it.

That is also the honest reason the aggregate on the next page groups notes by their wording and not by their meaning: mcpd never reads a sentence, at any point, in either direction.

The text is payload, and is treated as payload

A note is prose about a real conversation, and one a model wrote can repeat anything the model read. That makes notes the same kind of material as a prompt or a completion, and mcpd handles every note the same way. It passes through the redactor on the way in. Retention sweeps it on the same schedule as the trace it belongs to. Erasing a subject erases it, because a note inherits its trace's correlation and is scoped, filtered, retained and erased exactly as that trace is.

Reading notes back needs a read key, and every route that hands back a sentence is recorded in the audit ledger like every other endpoint that returns what a user wrote. See Redaction and retention for what never reaches storage and what stops being stored, and Security for the scopes and the ledger.

Next