Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 78 additions & 15 deletions docs/dev/adapter_observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

Epic #929 Phase 2, issue #1140. Covers three things landed together in the
same PR: the narrowed `AdapterMixin` verb contract, the shared
`resolve_model_options` helper, and the `AdapterFunctionMetricsPlugin` skeleton.
`resolve_model_options` helper, and the `AdapterFunctionMetricsPlugin` (added as a
skeleton by #1140; receiving real payloads as of #1141).

## AdapterMixin verb contract

Expand Down Expand Up @@ -45,9 +46,24 @@ a backend overrides only the verb matching its own adapter reality.
implements this yet; the verb name is defined for when that reality is
built.

`resolve_adapter()` and `adapter_scope()` are unchanged Phase 1 scaffolding
and out of scope for this issue — their real wiring into
`WeightsBinding.activate()`/`deactivate()` belongs to #1141/#1142.
`resolve_adapter()` is unchanged Phase 1 scaffolding — it still only knows about
`IntrinsicAdapter`/`LocalHFAdapter` and is not used to look up
`LocalFileBinding`/`Adapter` instances.

`adapter_scope()` is no longer scaffolding. As of #1141 it calls
`adapter.weights.activate()` before the `with` body and
`adapter.weights.deactivate()` after, in a `finally`, so deactivation runs even
if the body raises. Wiring the Granite Switch reality is #1142's job;
`adapter_scope()` itself does not change again for that.

Two new verbs arrived with #1141 for the LocalFile/PEFT reality:
`activate_peft_adapter` and `deactivate_peft_adapter`, extracted from the inline
`set_adapter(...)` calls that previously lived only inside
`LocalHFBackend._generate_with_adapter_lock`. Both document "must be called while
holding `_generation_lock`"; `AdapterMixin._adapter_activation_lock()` exists so
the `adapter_scope()` path satisfies that precondition, since unlike
`_generate_with_adapter_lock` it does not hold the lock already. See #1465 for the
reentrancy problem this creates once generation moves inside the scope.

## resolve_model_options

Expand All @@ -66,7 +82,7 @@ also routes through this helper for its `TEMPERATURE: 0.0` default, so
caller-supplied `model_options` can't be silently clobbered by a hardcoded
default — the same class of bug PR #972 fixed elsewhere.

## AdapterFunctionMetricsPlugin (skeleton)
## AdapterFunctionMetricsPlugin

`mellea/telemetry/metrics_plugins.py` adds `AdapterFunctionMetricsPlugin`, hooking
`adapter_function_invocation_complete` and `adapter_function_phase_complete`
Expand All @@ -81,17 +97,62 @@ default — the same class of bug PR #972 fixed elsewhere.
- `mellea.adapter_function.parse_failures` (counter) — labels: `name`, `revision`.
Incremented automatically whenever an invocation's `outcome` is
`schema_error` (i.e. an `AdapterSchemaMismatchError`), acting as a
schema-drift detector.

No production code fires these hooks yet — this is a skeleton, unit-tested
against synthetic payloads only (`test/telemetry/test_metrics_plugins.py`).
Real wiring from `prepare`/`activate`/`generate`/`parse`/`deactivate` is
expected to go in with #1141 (LocalFileBinding) and #1142 (EmbeddedBinding).
schema-drift detector. `adapter_scope()` classifies that exception separately
from a generic failure (#1141), so this counter is reachable rather than
structurally dead.

As of #1141, `LocalFileBinding.prepare()` and `adapter_scope()`'s
`activate`/`deactivate` phases fire `ADAPTER_FUNCTION_PHASE_COMPLETE`, and
`adapter_scope()` fires `ADAPTER_FUNCTION_INVOCATION_COMPLETE` on the way out —
all through the standard `has_plugins()`-then-`invoke_hook()` idiom, so the
metrics plugin receives real payloads whenever a `LocalFileBinding` is prepared
and driven through `adapter_scope()`.

Three phases still never fire:

- `release` has no `phase` `Literal` value to report against (`prepare` |
`activate` | `generate` | `parse` | `deactivate`), so
`LocalFileBinding.release()` fires nothing. That is #1140's contract, not a
#1141 oversight; #1466 reconciles the `Literal` with the firing sites.
- `generate` and `parse` need the production path to run inside
`adapter_scope()`, which it does not yet — `_generate_from_intrinsic` still
generates outside the scope. That cutover is **#1465**, and it is not "4.1"
(#1144, which is shim removal plus a docs rewrite).

`test/telemetry/test_metrics_plugins.py` still exercises the plugin against
synthetic payloads directly; it is not yet driven through a real invocation
end to end.

### How spans get produced (read this before adding any)

**Library code never opens spans.** It fires a hook; a plugin in
`mellea/telemetry/tracing_plugins.py` turns that hook into a span. Nothing under
`mellea/backends/` may import `mellea/telemetry/tracing`.

The rest of this document's span sections describe the intended span *shape*, and
that shape still stands. But the guidance further down — mirror
`start_backend_span` / `start_action_span` and call the `start_*_span` helpers
directly — describes the pre-#1181 pattern and is **wrong**. #1141's first
implementation followed it and produced spans inline inside `mellea/backends/`;
PR #1454 removed them again for that reason. The one file that does import those
helpers, `mellea/stdlib/session.py`, is a deliberate documented exception (OTel
`Token` attach/detach is task-affine), not a template.

There is also a structural gap: `ADAPTER_FUNCTION_INVOCATION_COMPLETE` and
`ADAPTER_FUNCTION_PHASE_COMPLETE` are the only `HookType` family with **no
pre/start sibling**, so a plugin currently has no event at which to open these
spans. The start hooks have to be added first.

- **#1464** rewrites this guidance and adds CI enforcement.
- **#1466** adds the start hooks and emits the adapter-function spans from a
plugin.

Until #1466, there is **no span coverage of the adapter-function lifecycle**.

## Span tree (structure)

Span *emission* ships with the Bindings (#1141/#1142) — no span code lands in
this PR. What this issue fixes is the *shape*, so the traces align with the
Spans for this lifecycle are not emitted yet — see the note above; #1466 owns
them. What #1140 fixed was the *shape*, so the traces align with the
metrics and follow Mellea's existing tracing conventions rather than a bespoke
scheme. Spans are opened through the `start_*_span` helper family in
`mellea/telemetry/tracing.py` (mirroring `start_backend_span` /
Expand Down Expand Up @@ -127,8 +188,10 @@ already use (it also honours `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT
**off by default**, so traces never capture PII or proprietary content unless
explicitly opted in. When unset or falsey, the phase spans carry metadata only;
when set truthy, they additionally attach the adapter's input/output content.
The adapter-function spans **reuse this gate rather than introducing a new one**;
content attributes are attached when the Bindings (#1141/#1142) emit spans.
The adapter-function spans **reuse this gate rather than introducing a new one**.
No content attributes are attached today, because no adapter-function spans are
emitted at all yet — #1466 adds them, and there is no adapter input/output content
to attach until `generate`/`parse` fire in production, which needs #1465.

(#1140's acceptance criteria named this `MELLEA_TRACE_CONTENT`; the real,
already-implemented variable is `MELLEA_TRACES_CONTENT` — see
Expand Down
Loading
Loading