@ssheleg/agent-stack 0.22.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,52 @@
1
+ ## v0.23.0 — the wire format for §7, and the three things it will not carry
2
+
3
+ `agent-evals` §7 said *what* to instrument in four bullets and named no format.
4
+ `references/otel-genai.md` is the format — and the reason it is worth its own file is not
5
+ the field list but the three places the standard refuses to carry what this skill requires.
6
+
7
+ **Read the status before encoding any of it.** The conventions have moved out of the main
8
+ `semantic-conventions` repository into their own, whose README lists **Schema URL: `TODO`**,
9
+ and every document is `Status: Development`. On the inference span the only `Stable`
10
+ attributes are the ones borrowed from core semconv; **every `gen_ai.*` attribute is
11
+ Development**. Adopt it anyway — a moving standard beats a private vocabulary nobody else's
12
+ tooling reads — but pin the version and treat any branch on a `gen_ai.*` name as code with
13
+ an expiry date.
14
+
15
+ **The evaluation event has no field for who produced the score.** `gen_ai.evaluation.result`
16
+ carries a name, a value, a low-cardinality label and an explanation, and nothing about the
17
+ scorer — while §7 requires exactly that (`human` | `llm_judge` | `code_check`), because a
18
+ score with no source cannot be calibrated or audited. OpenInference has the field under
19
+ another name, `annotation.annotator_kind`, with our three values. The reference says to carry
20
+ it as a documented extension rather than drop the requirement: a judge score and a human
21
+ label that are indistinguishable on the wire get averaged by somebody downstream, which is
22
+ what §5's calibration exists to prevent.
23
+
24
+ **There is no cost attribute at all**, and usage is eleven numbers rather than one —
25
+ `input_tokens`, `output_tokens`, `reasoning.output_tokens`, `cache_read.input_tokens`,
26
+ `cache_write.input_tokens`, plus per-modality splits. **A cost from `input + output` alone is
27
+ wrong in both directions**: it bills cache reads at full price and misses reasoning tokens
28
+ and cache writes entirely. `gen_ai.client.token.usage` carries a hard **MUST NOT report**
29
+ when counts are unobtainable — absence over a fabricated zero, the rule `audit.md` already
30
+ states for attribution.
31
+
32
+ **And two seams that decide a design rather than a field.** The content-upload hook *"SHOULD
33
+ operate independently of the opt-in flags"* and *"SHOULD be invoked regardless of the span
34
+ sampling decision"* — so it is the last scrubbing point and it fires on spans nobody will
35
+ read; a masking hook meanwhile **cannot change the span name**, which is built from tool and
36
+ MCP target names, so redaction and naming are one decision. Verified against a shipping
37
+ implementation rather than assumed: OpenLLMetry's `is_content_tracing_enabled()` returns
38
+ **true when its environment variable is unset**, the opposite of the spec's default, so
39
+ prompts are captured unless you turn them off (`traceloop-sdk/traceloop/sdk/config/__init__.py`,
40
+ read 2026-08-31).
41
+
42
+ **Finally, two words that mean three things each.** *"OpenTelemetry-based"* answers a
43
+ transport question and no semantic one: Phoenix emits OpenInference, OpenLLMetry emits
44
+ `gen_ai.*` plus `traceloop.*`, neither is a subset of the other, and a trace assembled from
45
+ both is two disconnected halves under one trace id. And *"replay"* is durable execution
46
+ (recorded results, nothing re-runs), a trace playground (the call re-runs against the live
47
+ provider), or this skill's fixture replay (an assertion over a stored run) — `audit.md` asks
48
+ whether a run can be replayed without saying which, and the reference names the three.
49
+
1
50
  ## v0.22.0 — the failures named from outside, and the control that is absence
2
51
 
3
52
  **`graph-engineering.md` was argued entirely from what breaks, with no citation behind it.**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ssheleg/agent-stack",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "scripts": {
5
5
  "test": "python3 test/validate.py && python3 test/plant_guard_test.py && node test/installer_test.js"
6
6
  },
@@ -3,7 +3,7 @@
3
3
  "name": "agent-stack",
4
4
  "displayName": "Agent Stack",
5
5
  "description": "Four skills: agent-orchestrator — tool-calling loops, pipelines with checkpoints, provider routing with fallback, memory architecture, plus the wallet side of reselling LLM access; agent-evals — run/trace/thread evals, LLM judges, and fixtures grown from production; agent-interop — MCP servers and clients, A2A agent cards, the MCP Registry, and gateways; agent-harness — system prompts, tool shaping, workflow-vs-agent, and auditing an agent system.",
6
- "version": "0.22.0",
6
+ "version": "0.23.0",
7
7
  "author": {
8
8
  "name": "ssheleg",
9
9
  "url": "https://x.com/sshlg93"
@@ -288,6 +288,10 @@ predictive.
288
288
 
289
289
  ## 7. What to instrument first
290
290
 
291
+ **The wire format for all of this is `references/otel-genai.md`** — and it is worth
292
+ reading before the first span, because two of the things this section requires (a score's
293
+ source, a trustworthy cost) are the two the standard does not carry.
294
+
291
295
  None of the above runs without these, and they are the part people skip:
292
296
 
293
297
  - **A durable trace store, queryable by id, filterable by score and time.** The live
@@ -330,6 +334,7 @@ None of the above runs without these, and they are the part people skip:
330
334
  | Load | When |
331
335
  |---|---|
332
336
  | [`references/statistics.md`](references/statistics.md) | a number is about to change a decision — how many runs before a difference is real, `pass@k` vs `pass^k` and why trials are not independent, paired comparison, the harness as a variable, and what a given piece of evidence authorises next |
337
+ | [`references/otel-genai.md`](references/otel-genai.md) | you are **instrumenting an agent for someone else to read** — span-name formulas and the closed operation enum, the evaluation event that has no field for who scored, the three content tiers and the upload hook that runs even on dropped spans, the eleven token attributes and why cost is a join you can get wrong in both directions, and why "OpenTelemetry-based" is not one vocabulary |
333
338
 
334
339
  ---
335
340
 
@@ -0,0 +1,177 @@
1
+ # OpenTelemetry GenAI — the wire format for §7, and what it does not carry
2
+
3
+ **Load this when instrumenting an agent for someone else to read**: choosing span names and
4
+ attributes, deciding what of a prompt reaches a backend, wiring token usage into a bill, or
5
+ judging what an "OpenTelemetry-based" tool actually gives you.
6
+
7
+ §7 says *what* to instrument in four bullets. This is the format that carries it — and the
8
+ three places it will not carry what this skill requires.
9
+
10
+ ## Contents
11
+
12
+ - [Read this before encoding any of it](#read-this-before-encoding-any-of-it)
13
+ - [Span names are formulas, and the operation list is closed](#span-names-are-formulas-and-the-operation-list-is-closed)
14
+ - [The evaluation event, missing the field §7 requires](#the-evaluation-event-missing-the-field-7-requires)
15
+ - [Content: three tiers, and a hook that runs when nothing else does](#content-three-tiers-and-a-hook-that-runs-when-nothing-else-does)
16
+ - [Tokens are eleven numbers and money is none of them](#tokens-are-eleven-numbers-and-money-is-none-of-them)
17
+ - ["OpenTelemetry-based" is not one vocabulary](#opentelemetry-based-is-not-one-vocabulary)
18
+ - [Replay is three different guarantees](#replay-is-three-different-guarantees)
19
+
20
+ ---
21
+
22
+ ## Read this before encoding any of it
23
+
24
+ **The conventions moved.** They no longer live in the main `semantic-conventions` repository
25
+ — that page now says only that they have moved and is no longer maintained. They have their
26
+ own repository, whose README lists **Schema URL: `TODO`**.
27
+
28
+ **Nothing in them is stable.** Every document is marked `Status: Development`. On the
29
+ inference span the only attributes marked `Stable` are the ones borrowed from core semconv —
30
+ `error.type`, `server.address`, `server.port`, `exception.*`. **Every single `gen_ai.*`
31
+ attribute is `Development`.**
32
+
33
+ So: adopt it, because a moving standard beats a private vocabulary that will never be read
34
+ by anyone else's tooling — and **pin the version you adopted and expect to migrate**. Treat
35
+ any code that branches on a `gen_ai.*` attribute as code with an expiry date, and re-read
36
+ the spec before quoting a field name from this file.
37
+
38
+ ## Span names are formulas, and the operation list is closed
39
+
40
+ Span names are computed, not free text:
41
+
42
+ | Span | Name | Kind |
43
+ |---|---|---|
44
+ | inference | `{gen_ai.operation.name} {gen_ai.request.model}` | `CLIENT` |
45
+ | tool call | `execute_tool {gen_ai.tool.name}` | `INTERNAL` |
46
+ | agent invocation | `invoke_agent {gen_ai.agent.name}` | `CLIENT` |
47
+ | planning | `plan {gen_ai.agent.name}` | `INTERNAL` |
48
+ | agent creation | `create_agent {gen_ai.agent.name}` | — |
49
+ | MCP | `{mcp.method.name} {target}`, target being the tool or prompt name | — |
50
+
51
+ `gen_ai.operation.name` is a **closed 17-value enum** — `chat`, `text_completion`,
52
+ `generate_content`, `embeddings`, `retrieval`, `fetch_response`, `execute_tool`,
53
+ `create_agent`, `invoke_agent`, `plan` and the rest. A value outside it is not an extension,
54
+ it is a name a backend cannot group by.
55
+
56
+ **Only two attributes are Required on an inference span.** Everything else that matters —
57
+ the model that actually answered, token counts, finish reasons — is Recommended or
58
+ Conditionally Required. A conformant instrumentation can therefore be almost empty, which is
59
+ the reason to specify what *you* need rather than to trust conformance as a floor.
60
+
61
+ One behaviour worth knowing before you count spans: **automatic retries collapse into one
62
+ span.** A span is one logical operation, not one HTTP request, so a retry storm is invisible
63
+ at this layer and has to be measured somewhere else.
64
+
65
+ ## The evaluation event, missing the field §7 requires
66
+
67
+ The spec defines `gen_ai.evaluation.result`, parented to the span being evaluated — or
68
+ carrying `gen_ai.response.id` when the span id is not available — with:
69
+
70
+ - `gen_ai.evaluation.name` (**Required**)
71
+ - `gen_ai.evaluation.score.value`
72
+ - `gen_ai.evaluation.score.label` — low-cardinality: `pass`, `fail`, `relevant`, …
73
+ - `gen_ai.evaluation.explanation`
74
+
75
+ **There is no attribute for who or what produced the score.**
76
+
77
+ That is precisely the field this skill requires: §7 says a score binds to a run with a
78
+ `source` of `human` | `llm_judge` | `code_check`, *because a score with no source cannot be
79
+ calibrated, audited, or trusted differently from its neighbours*. The standard omits it.
80
+
81
+ OpenInference has it, under a different name: **`annotation.annotator_kind`** — `HUMAN`,
82
+ `LLM`, `CODE`, or custom — with `evaluation.annotator_kind` beside it. The three values are
83
+ our three values.
84
+
85
+ **So carry it yourself.** Add the attribute under your own namespace and document that it is
86
+ an extension; do not drop the requirement because the schema has no slot for it. A judge
87
+ score and a human label that are indistinguishable on the wire will be averaged by somebody
88
+ downstream, and that average is the thing §5's calibration exists to prevent.
89
+
90
+ ## Content: three tiers, and a hook that runs when nothing else does
91
+
92
+ Prompt and completion content is governed by three named patterns, chosen by environment:
93
+
94
+ 1. **Record nothing** — the default.
95
+ 2. **Record on span attributes** — *"best suited for … pre-production environments"*.
96
+ 3. **Store externally, record a reference on the span** — *"recommended in production
97
+ environments where telemetry volume is a concern or sensitive data needs to be handled
98
+ securely. Using external storage enables separate access controls."*
99
+
100
+ **The spec's own default is to capture nothing**, and it says instrumentations SHOULD NOT
101
+ capture content by default. Verify that in your stack rather than assuming it: the most
102
+ widely used `gen_ai.*` implementation ships the opposite. OpenLLMetry's
103
+ `is_content_tracing_enabled()` reads
104
+
105
+ ```python
106
+ return (os.getenv("TRACELOOP_TRACE_CONTENT") or "true").lower() == "true"
107
+ ```
108
+
109
+ — an unset variable is **true**, so prompts are captured unless you turn them off. Measured
110
+ 2026-08-31 against `traceloop-sdk/traceloop/sdk/config/__init__.py`. An agent's trace carries
111
+ prompts, prompts carry secrets, and the default here works against you.
112
+
113
+ **The upload hook is the last scrubbing point, and it runs where nothing else does.** Two
114
+ properties make it the mechanism rather than a convenience: it *"SHOULD operate independently
115
+ of the opt-in flags"*, and instrumentations *"SHOULD invoke it regardless of the span sampling
116
+ decision"*. It therefore fires on spans nobody will ever look at — which is right, because
117
+ the content has already left the process by then, and wrong to reason about as *"we only keep
118
+ sampled traces"*.
119
+
120
+ **A masking hook cannot change the span name.** Span names are built from tool names and MCP
121
+ targets, so a redaction design that scrubs attributes and leaves the name is a design that
122
+ leaks through the one field it never inspected. Decide naming and redaction together.
123
+
124
+ ## Tokens are eleven numbers and money is none of them
125
+
126
+ **There is no cost attribute anywhere in the GenAI semantic conventions.** The spec
127
+ standardises tokens and never money, so cost is always a join against a price table living
128
+ outside the trace — and that join is where the number goes wrong.
129
+
130
+ Because usage is not one number. It is eleven: `gen_ai.usage.input_tokens`, `output_tokens`,
131
+ `reasoning.output_tokens`, `cache_read.input_tokens`, `cache_write.input_tokens`, and
132
+ per-modality `text.*` / `image.*` / `audio.*` splits including
133
+ `image.cache_read.input_tokens`.
134
+
135
+ **A cost computed from `input_tokens + output_tokens` alone is wrong in both directions.** It
136
+ bills cache reads at full price — they are the cheap ones — and it misses reasoning tokens and
137
+ cache writes entirely. `references/kv-cache.md` in `agent-orchestrator` is the other half of
138
+ this: the cache read is the case worth getting right, because at scale it is most of the
139
+ traffic.
140
+
141
+ And `gen_ai.client.token.usage` carries a hard **MUST NOT report** when the counts are not
142
+ obtainable. A zero is a claim; absence is the honest value. That is the same rule
143
+ `agent-harness/references/audit.md` states for cost attribution — *missing attribution beats
144
+ wrong attribution, and absence is a named state rather than a fabricated zero*.
145
+
146
+ ## "OpenTelemetry-based" is not one vocabulary
147
+
148
+ Two widely used tools both describe themselves as OpenTelemetry-based and emit **disjoint**
149
+ attribute sets:
150
+
151
+ | | Emits |
152
+ |---|---|
153
+ | **Phoenix** | OpenInference — `openinference.span.kind` (required; `LLM`, `CHAIN`, `RETRIEVER`, `RERANKER`, `TOOL`, `AGENT`, `GUARDRAIL`, `EVALUATOR`, `PROMPT`, `EMBEDDING`), plus `llm.*`, `input.value`, `input.mime_type`, `document.*`, `annotation.*` |
154
+ | **OpenLLMetry** | `gen_ai.*` plus `traceloop.*` |
155
+
156
+ **Neither set is a subset of the other, and neither is a superset of the GenAI
157
+ conventions.** A backend query written against one returns nothing against the other, and a
158
+ trace assembled from two services using each is **two disconnected halves under one trace
159
+ id** — which reads as a gap in the system rather than a gap in the vocabulary.
160
+
161
+ So *"we use OpenTelemetry"* answers a transport question and no semantic one. Ask which
162
+ attribute set, and pick one per system rather than per service.
163
+
164
+ ## Replay is three different guarantees
165
+
166
+ The word appears in three products in this space and means three incompatible things. Our
167
+ own `agent-harness/references/audit.md` asks *"can a past run be replayed"* without saying
168
+ which:
169
+
170
+ | Sense | What re-runs | Cost | Answers |
171
+ |---|---|---|---|
172
+ | **Durable execution** (Temporal) | nothing — recorded results are replayed and only the failed step retries | free, deterministic | can I resume without redoing 20 web searches |
173
+ | **Trace playground** (Phoenix) | the model call, against the live provider, with an edited prompt | a real call | would a different prompt have done better |
174
+ | **Fixture replay** (this skill, §2 single-step) | an assertion over a stored run | free | did the decision at this point change |
175
+
176
+ They are not interchangeable, and a runbook that says *"replay the run"* has not said what it
177
+ means. Name the sense.