You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Parent epic:#929 Phase: cross-cutting (do before #1142 starts) Depends on: nothing Blocks:#1466
Problem
Mellea has a hard rule about how spans are produced — library code fires a hook,
and a plugin in mellea/telemetry/tracing_plugins.py turns that hook into a
span. Library code never opens spans itself. That rule is nowhere written down,
and nothing enforces it, so it keeps being broken by people acting in good faith.
The history matters, because it explains why "just read the docs" is not
currently an answer:
Until refactor(telemetry)!: move backend tracing onto plugin/hook pattern #1181 (2026-06-10, refactor(telemetry)!), calling the span helpers
directly from library code was the pattern. All five backends contained a
bare span = start_generate_span(...), with the supporting machinery in mellea/telemetry/backend_instrumentation.py.
refactor(telemetry)!: move backend tracing onto plugin/hook pattern #1181 rewrote around 97 lines of docs/docs/observability/tracing.md — and
that page still contains no occurrence of the word "plugin". It documents
which spans exist and what attributes they carry, never how a span comes to be
produced.
The only in-code example of importing telemetry.tracing is mellea/stdlib/session.py, which is a deliberate, documented exception
(OTel Token attach/detach is task-affine). Anyone using it as a template
copies the exception rather than the pattern.
There is also a structural gap that makes the rule impossible to follow for
adapter functions specifically. ADAPTER_FUNCTION_INVOCATION_COMPLETE and ADAPTER_FUNCTION_PHASE_COMPLETE are the only HookType family with no
pre/start sibling, so there is no event at which a plugin could open those spans.
Documenting the rule without noting this would set an unmeetable expectation.
Scope
docs/docs/observability/tracing.md — explain how spans are produced, not just
which spans exist: hooks fire, plugins emit, and why (telemetry stays optional
and testable without an exporter).
Record the session.py exception as an exception, at a place a reader will find
before copying it.
docs/dev/adapter_observability.md — correct the direct-start_*_span
guidance.
Enforcement in CI, so the next violation fails a check rather than relying on
review attention. Suggested boundary: nothing under mellea/backends/ imports mellea/telemetry/tracing, with mellea/stdlib/session.py as the sole
allowlisted exception.
Parent epic: #929
Phase: cross-cutting (do before #1142 starts)
Depends on: nothing
Blocks: #1466
Problem
Mellea has a hard rule about how spans are produced — library code fires a hook,
and a plugin in
mellea/telemetry/tracing_plugins.pyturns that hook into aspan. Library code never opens spans itself. That rule is nowhere written down,
and nothing enforces it, so it keeps being broken by people acting in good faith.
The history matters, because it explains why "just read the docs" is not
currently an answer:
refactor(telemetry)!), calling the span helpersdirectly from library code was the pattern. All five backends contained a
bare
span = start_generate_span(...), with the supporting machinery inmellea/telemetry/backend_instrumentation.py.This was the right change.
docs/docs/observability/tracing.md— andthat page still contains no occurrence of the word "plugin". It documents
which spans exist and what attributes they carry, never how a span comes to be
produced.
invisible to CI.
telemetry.tracingismellea/stdlib/session.py, which is a deliberate, documented exception(OTel
Tokenattach/detach is task-affine). Anyone using it as a templatecopies the exception rather than the pattern.
docs/dev/adapter_observability.mdwas written anddescribes the pre-refactor(telemetry)!: move backend tracing onto plugin/hook pattern #1181 approach, naming
start_backend_span/start_action_spanas the model to mirror.mellea/backends/. They are being removed in PR feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454.Two live consequences:
docs/dev/adapter_observability.mdstill instructs the next implementer to dothe same thing.
feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142's telemetry acceptance criteria mandate the same defect. feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142 is now
unblocked, specified spans "under the
intrinsic.callparent", and names tests(
test_prepare_span_binding_type_is_embedded,test_activate_span_has_controls_key). Whoever picks it up will reproducefeat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1454's problem by following the issue as written.
Partly addressed, 2026-08-11. feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142 has since been rescoped (
apply_activation,no lifecycle verbs) and its span criteria now carry an explicit "emitted from a
tracing plugin, not from library code" qualifier and a
blocked on #1464 + #1466marker, so the mechanism half of this consequence is defused. The invented
intrinsic.*span names were also corrected across feat(backends): LocalFileBinding implements verbs (PEFT/aLoRA path) + from_catalog() (Epic #929 Phase 2) #1141, feat(backends): EmbeddedBinding implements apply_activation (Granite Switch path); remove render_controls + set_request_adapter (Epic #929 Phase 2) #1142 and feat(telemetry): emit adapter-function spans from a tracing plugin (Epic #929 Phase 2) #1466 the sameday. What remains for this issue is the durable half: the page that taught the wrong
pattern, and CI enforcement so the next occurrence fails a check rather than needing
a reviewer to spot it.
There is also a structural gap that makes the rule impossible to follow for
adapter functions specifically.
ADAPTER_FUNCTION_INVOCATION_COMPLETEandADAPTER_FUNCTION_PHASE_COMPLETEare the onlyHookTypefamily with nopre/start sibling, so there is no event at which a plugin could open those spans.
Documenting the rule without noting this would set an unmeetable expectation.
Scope
docs/docs/observability/tracing.md— explain how spans are produced, not justwhich spans exist: hooks fire, plugins emit, and why (telemetry stays optional
and testable without an exporter).
session.pyexception as an exception, at a place a reader will findbefore copying it.
docs/dev/adapter_observability.md— correct the direct-start_*_spanguidance.
review attention. Suggested boundary: nothing under
mellea/backends/importsmellea/telemetry/tracing, withmellea/stdlib/session.pyas the soleallowlisted exception.
pre-refactor(telemetry)!: move backend tracing onto plugin/hook pattern #1181 pattern.
ADAPTER_FUNCTION_*family where the ruleis documented, so the exception is visible rather than surprising.
Out of scope
Adding the missing start hooks, and emitting adapter-function spans — both belong
to #1466, which this issue blocks.
Acceptance criteria
docs/docs/observability/tracing.mdstates the hook-fires/plugin-emits ruleand contains at least one worked reference to
tracing_plugins.pymellea/stdlib/session.pyexception is documented as suchdocs/dev/adapter_observability.mdno longer instructs implementers to callstart_*_spanfrom library codetelemetry.tracingimport undermellea/backends/, withsession.pyallowlisted(evidence in the PR description; the temporary change is not committed)
npx markdownlint-cli2passes on every page touchedReferences
mellea/telemetry/tracing_plugins.py— the four existing span plugins