1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
//! The observability vocabulary, defined once.
//!
//! # Why the names live here and not at the call sites
//!
//! A span or event name typed inline at twelve call sites is twelve chances to
//! drift, and drift in telemetry is invisible: the dashboard simply stops
//! matching and nobody is told. These constants are the contract, and
//! `tests/guards/layering.rs` checks that every event P7 promises is actually emitted
//! by something.
//!
//! Span names, event names, and *span* attribute keys are taken from these
//! constants at every call site. Field names inside `tracing` **events** are
//! short idents instead — the event macro accepts only idents there, not
//! computed keys — so an event's semantic identity lives in its `target`, which
//! is a constant. That is the whole reason events are targeted rather than
//! merely named.
//!
//! # What is instrumented, and why that set
//!
//! Principle P7 is *no silent anything*. The failures this runtime exists to
//! surface — divergence, an undecidable outcome, a refused budget, a
//! compensation that could not run, an event nobody claimed — are exactly the
//! ones that otherwise present as a process quietly not finishing. Each has a
//! dedicated event here, so "did this happen" is a query rather than an
//! archaeology exercise over logs.
//!
//! # Replay is marked, always
//!
//! A replayed run re-executes its skills, so it emits spans again. Without a
//! mode attribute an operator sees each run twice and metrics like "effect
//! latency by driver" silently average real calls with journal reads. Every
//! span carries [`MODE`].
//!
//! For effects the marking is sharper than an attribute, and a bridge author
//! should know exactly what it is: **a replayed effect opens no span at all.**
//! It never reaches the world, so there is no duration to record; what it emits
//! instead is a `debug` **event** on the [`EFFECT_SPAN`] target carrying
//! `replayed = true`, plus the `agentplane.effects_replayed` counter. The
//! [`EFFECT_REPLAYED`] field on a live effect span is therefore always `false`,
//! and it is there so a collector filtering on one attribute name gets a
//! uniform answer rather than an absent field.
//!
//! The consequence is the one that matters: a span-derived latency histogram is
//! clean **by construction**, because replays contribute no spans to it. What a
//! bridge must not do is key on the *target* and treat every record on it as a
//! span — that view sees both, and it is the view in which replays contaminate
//! latency. `examples/observability.rs` does it the correct way and asserts on
//! the difference.
//!
//! # Semantic conventions
//!
//! OpenTelemetry's `GenAI` conventions still classify **agent spans as
//! Development**, and the June 2026 repository split confirms the work is
//! mid-flight rather than finished. Following `main` would mean silently
//! changing an operator's dashboards. So a pinned snapshot is vendored here and
//! exposed as [`SEMCONV_VERSION`]; upstream movement is a versioned migration,
//! not something that happens to you.
//!
//! # What is deliberately never emitted
//!
//! The conventions define Opt-In attributes that carry the content of a call:
//! `gen_ai.input.messages`, `gen_ai.output.messages`,
//! `gen_ai.system_instructions`, `gen_ai.tool.definitions` and
//! `gen_ai.tool.call.arguments`. This crate emits none of them, and the decision
//! is not a default anyone may flip: a prompt is exactly the place governed
//! values arrive, the sink gates exist to keep those values inside a declared
//! ceiling, and a trace exporter is an egress the gates do not cover. Sensitivity
//! is a property of the value, and a span is not a sink that can carry one.
//!
//! What a deployment gets instead is the shape of the call — who was asked, what
//! it cost, which tool ran, how it ended — and the journal for the content, where
//! the same values are sealed, labelled and erasable.
//!
//! Three more the convention defines that this plane does not emit, each for its
//! own reason. `gen_ai.response.id`, because no shared part of a [`Completion`]
//! carries one. `gen_ai.provider.name` on the **run** span, because the agent is
//! executed here rather than served by anyone — the completion spans beneath it
//! each name the provider that answered them. And `server.address`, because a
//! driver's endpoint is deployment configuration that is identical for every call
//! it makes: that belongs on the `OTel` *resource*, which the embedder owns,
//! rather than repeated on every span this crate opens.
//!
//! [`Completion`]: crate::model::Completion
/// The pinned `GenAI` semantic-convention snapshot these names follow.
///
/// Emitted on the run span so a collector can tell which vocabulary produced a
/// trace, and so a future change is a visible migration rather than a silent
/// reinterpretation of old data.
pub const SEMCONV_VERSION: &str = "genai-2026-07-28/development";
// ── Span names ──────────────────────────────────────────────────────────────
/// One per run. The trace root.
pub const RUN_SPAN: &str = "agentplane.run";
/// One per step execution, including a compensating pass.
pub const STEP_SPAN: &str = "agentplane.step";
/// One per effect *attempt*, so a retried call shows as several.
pub const EFFECT_SPAN: &str = "agentplane.effect";
// ── Attributes ──────────────────────────────────────────────────────────────
/// From the `GenAI` conventions: this span is an agent invocation.
pub const GEN_AI_OPERATION: &str = "gen_ai.operation.name";
/// The value of [`GEN_AI_OPERATION`] for a run.
pub const GEN_AI_INVOKE_AGENT: &str = "invoke_agent";
/// The value of [`GEN_AI_OPERATION`] for a tool call.
pub const GEN_AI_EXECUTE_TOOL: &str = "execute_tool";
/// The value of [`GEN_AI_OPERATION`] for a model completion.
pub const GEN_AI_CHAT: &str = "chat";
/// Which provider served a completion — `anthropic`, `openai`.
pub const GEN_AI_PROVIDER: &str = "gen_ai.provider.name";
/// The model asked for, which is not always the model that answered.
pub const GEN_AI_REQUEST_MODEL: &str = "gen_ai.request.model";
/// The model that answered, as the provider reported it.
///
/// Absent where the wire does not say — never filled from the request, which
/// would report a substitution had been ruled out when nothing looked.
pub const GEN_AI_RESPONSE_MODEL: &str = "gen_ai.response.model";
/// Why generation stopped, in the provider's own words.
///
/// The convention's name is plural because a provider may return several
/// candidates. A [`Completion`](crate::model::Completion) carries exactly one
/// answer and therefore one reason, so the attribute holds a single value.
pub const GEN_AI_FINISH_REASON: &str = "gen_ai.response.finish_reasons";
/// Prompt tokens billed.
pub const GEN_AI_INPUT_TOKENS: &str = "gen_ai.usage.input_tokens";
/// Completion tokens billed.
pub const GEN_AI_OUTPUT_TOKENS: &str = "gen_ai.usage.output_tokens";
/// Of the prompt tokens, how many came from a provider-managed cache.
///
/// Recorded because the *rate* differs by about a factor of ten: a panel adding
/// input tokens alone over-states a heavily cached deployment's bill on exactly
/// the portion it is cheapest on.
pub const GEN_AI_CACHE_READ_TOKENS: &str = "gen_ai.usage.cache_read.input_tokens";
/// Of the prompt tokens, how many were written into that cache — billed at a
/// premium over ordinary input, which is the other half of the same argument.
pub const GEN_AI_CACHE_WRITE_TOKENS: &str = "gen_ai.usage.cache_write.input_tokens";
/// The tool a call named, for `execute_tool` spans.
pub const GEN_AI_TOOL_NAME: &str = "gen_ai.tool.name";
/// Which declaration a run is executing.
///
/// The convention's name for it, rather than one in this crate's namespace: an
/// agent's name is a fact the convention already defines, and a second spelling
/// is read by nothing generic.
pub const GEN_AI_AGENT_NAME: &str = "gen_ai.agent.name";
/// The thread a run belongs to — this plane's case.
///
/// A case is what the A2A surface already answers `contextId` with, and
/// `contextId` is that protocol's conversation identifier, so the mapping is not
/// this crate's invention.
pub const GEN_AI_CONVERSATION_ID: &str = "gen_ai.conversation.id";
/// The class of fault an attempt ended with, from `OpenTelemetry`'s own
/// cross-signal attribute rather than a `GenAI` one.
///
/// Separate from [`OUTCOME`], which says *whether* an attempt succeeded. This
/// says what went wrong, in the vocabulary [`EffectError`] classifies faults
/// with — so "which driver fails how" is a group-by rather than a search
/// through free text.
///
/// [`EffectError`]: crate::core::EffectError
pub const ERROR_TYPE: &str = "error.type";
pub const RUN_ID: &str = "agentplane.run.id";
/// Which tenant's plane produced this run, under [`TenantLabel`].
///
/// Absent under the default policy, which is what `Omitted` means — not an empty
/// string, so a collector cannot mistake *not disclosed* for *no tenant*.
pub const TENANT: &str = "agentplane.tenant";
/// Which `GenAI` convention snapshot produced this trace.
pub const SEMCONV: &str = "agentplane.semconv";
/// `live` | `resume` | `strict` — see the module docs on why this is on
/// every span.
pub const MODE: &str = "agentplane.mode";
pub const STEP: &str = "agentplane.step.id";
pub const CAPABILITY: &str = "agentplane.step.capability";
/// `forward` | `compensating`.
pub const PHASE: &str = "agentplane.phase";
pub const EFFECT_KIND: &str = "agentplane.effect.kind";
/// The journal's own identity for this attempt.
///
/// The join between a trace and the evidence. Everything else on the span names
/// a *category* — which run, which step, which kind — and none of them lands a
/// reader on the record that attempt wrote; `GET /runs/{run}/history` answers by
/// key. Without it the two histories of one run can only be lined up by eye.
///
/// A digest over the step, phase, ordinal, attempt, kind and canonical
/// arguments. It **names** the attempt without carrying what it sent — which is
/// the claim to make, since a digest is still a commitment to the values it was
/// taken over and anyone holding a guess can test it.
pub const EFFECT_KEY: &str = "agentplane.effect.key";
pub const EFFECT_ATTEMPT: &str = "agentplane.effect.attempt";
pub const EFFECT_MUTATES: &str = "agentplane.effect.mutates";
/// True when the result came from the journal rather than from the world.
///
/// Without this, "effect latency by driver" averages real calls with journal
/// reads and means nothing.
pub const EFFECT_REPLAYED: &str = "agentplane.effect.replayed";
pub const OUTCOME: &str = "agentplane.outcome";
// ── Tenant attribution ──────────────────────────────────────────────────────
/// Whether and how a tenant appears on what this plane reports about itself.
///
/// Two separate problems, and a design that answers only one of them is worse
/// than none.
///
/// **Cardinality.** An unbounded label is what makes a metrics backend fall
/// over, and a tenant read from a *request* would be exactly that. This label is
/// the plane's own tenant, so the number of streams is the number of planes an
/// operator configured — a configuration fact, not a data fact. There is no
/// input that can grow it.
///
/// One policy for **both** signals. A deployment that decides customer names may
/// travel to its observability stack has decided it once, and answering *which
/// tenant is this* on the metrics while leaving it unanswerable on the traces is
/// the same decision honoured on one channel.
///
/// **Disclosure.** A tenant name is frequently a customer name, and a metrics
/// backend is usually the least protected system in a deployment: sampled into
/// third-party services, on a dashboard nobody signs into, retained past every other
/// record. So the default is to emit nothing.
// ── Events: the ones P7 exists for ──────────────────────────────────────────
/// Replay recomputed a different effect key than the journal holds.
pub const NONDETERMINISM: &str = "agentplane.run.nondeterminism_detected";
/// A run was set aside for a human.
pub const QUARANTINED: &str = "agentplane.run.quarantined";
/// A person closed a run whose outcome could never be established.
///
/// The end of the road for a quarantine nobody could answer, and it fires
/// **because nothing was unwound**: what the run left in the world stays there,
/// and the doubt is permanent. The person who decided that already knows; this
/// is for everybody else — the party who answers for an unexplained mutation is
/// rarely the operator holding the pager at 3 a.m., and an intervention visible
/// only to whoever made it is not oversight.
///
/// The lasting record is the `agentplane audit` finding, which is derived from
/// the journal and outlives every status. This is the notification that the
/// finding now exists.
pub const ABANDONED: &str = "agentplane.run.abandoned";
/// A run concluded `failed`, with the reason it gives an operator.
///
/// A failure is an ordinary conclusion here rather than an incident — it stays
/// open, findable under `GET /runs?outcome=failed`, and resumable. But *ordinary*
/// is not *invisible*: an operator running the shipped server had no way at all
/// to learn why a run failed, because the outcome index needs the HTTP surface
/// and the reason otherwise reached only the journal. That is I13's
/// detection-without-delivery on the most common conclusion there is. The
/// reason string is the same one the outcome index already hands an operator,
/// so this discloses nothing new — it delivers it to somebody watching.
pub const RUN_FAILED: &str = "agentplane.run.failed";
/// An outcome could not be determined and guessing was forbidden.
pub const UNDECIDABLE: &str = "agentplane.effect.undecidable";
/// A read pinned by digest or version came back different.
///
/// Names one run, and says nothing about which *others* read the same version —
/// only an audit answers that, which is what this event exists to start.
pub const UNREPRODUCIBLE: &str = "agentplane.run.unreproducible";
/// A probe was asked whether a call landed.
pub const RECONCILED: &str = "agentplane.effect.reconciled";
/// A limit refused an operation before it started.
pub const BUDGET_REFUSED: &str = "agentplane.budget.refused";
/// A completed step was undone.
pub const COMPENSATED: &str = "agentplane.saga.compensated";
/// A compensation failed, leaving the run partly unwound.
pub const COMPENSATION_FAILED: &str = "agentplane.saga.compensation_failed";
/// An event aged out with nobody waiting for it — a correlation bug somewhere.
pub const DEAD_LETTERED: &str = "agentplane.event.dead_lettered";
/// An obligation passed unmet.
pub const DEADLINE_BREACHED: &str = "agentplane.deadline.breached";
/// A sleeping run's instant arrived.
pub const TIMER_FIRED: &str = "agentplane.timer.fired";
/// A run an instance died holding was taken over and resumed.
pub const RUN_RECOVERED: &str = "agentplane.run.recovered";
/// Policy refused an action before it was attempted.
pub const POLICY_DENIED: &str = "agentplane.policy.denied";
/// A run changed its plan; the successor is journaled beside its predecessor.
pub const REPLANNED: &str = "agentplane.run.replanned";
/// A witness refused this plane's checkpoint on integrity grounds.
///
/// The log shrank, forked, or claimed growth a witness could not verify — the
/// one class of event nothing inside this plane can detect, because every
/// input to the chain, the signatures and the Merkle log comes from the party
/// an auditor is being asked to trust.
///
/// Loud for a reason no other event here has: the audience is *not* the
/// operator running the plane. A witness reporting that this history moved is
/// the finding whoever operates it has the strongest interest in nobody
/// hearing, so it goes to the same channel every other unexplained mutation
/// does rather than staying in a sweep report the plane's own operator reads.
/// It fires even when the declared quorum was met — two honest cosigners do
/// not answer a third that remembers a different history.
pub const WITNESS_INTEGRITY: &str = "agentplane.witness.integrity";
/// Every event name P7 promises, for the guard in `tests/guards/layering.rs`.
///
/// A constant nobody emits is the telemetry equivalent of a dead API: the
/// dashboard has a panel and the panel is always empty.
pub const LOUD_EVENTS: & = &;
/// How a run is being executed, as a span attribute.
pub const