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
Emit implementation attribution on every invocation (#132)
* Emit implementation attribution on every invocation
Implements proposal 0052 (spec observability 5.1 + 8.4.1,
v0.44.0). Two new attributes -- openarmature.implementation.name
and openarmature.implementation.version -- emit on every OTel
invocation span alongside the existing openarmature.graph.spec_version.
The Langfuse observer mirrors the rows as
trace.metadata.implementation_name and trace.metadata.implementation_version
on every Trace. The values let observability-backend operators
answer "which library, at which version, produced this trace"
without a separate deployment-manifest lookup.
The name value is the canonical PyPI package name
("openarmature-python"), pinned as a new __implementation_name__
constant at the package root next to __version__ and
__spec_version__. The version value is __version__ itself -- no
separate __implementation_version__ constant to avoid the
maintenance trap of two values that have to stay in lockstep.
Both observers expose configurable implementation_name and
implementation_version dataclass fields for test parameterization,
defaulting to the package identity via lazy-import helpers that
mirror the existing _read_spec_version pattern. The OTel invocation
span emits the attributes once per invocation (per 5.1, not the
cross-cutting 5.6 family -- inner-node spans do NOT carry them).
The Langfuse rows emit on both trace-open paths: the proposal 0043
boundary-event lazy path and the legacy NodeEvent path.
Always-emit invariant pinned on both observers: neither
disable_state_payload, disable_llm_payload, disable_llm_spans, nor
disable_genai_semconv gates the attributes, since they describe
runtime identity rather than runtime data.
The 3.4 reserved-key set extends from 24 to 26 names:
implementation_name and implementation_version reject
caller-supplied collision at the invoke() boundary, so a caller
passing invocation_metadata={"implementation_name": "spoof"}
gets ValueError rather than silently clobbering the
implementation-emitted value.
Twelve new unit tests pin the contract: 6 reserved-key (validate /
set / invoke boundary for both names) + 3 OTel (invocation-span
presence + inner-span absence, always-emit under disable_llm_payload,
multi-invocation reuse) + 3 Langfuse (trace metadata presence,
always-emit under disable_state_payload, multi-invocation reuse).
Conformance fixtures 058 (OTel) + 059 (Langfuse) stay deferred
from the cross-capability parser pending the upcoming
conformance-adapter capability spec.
* Document new observer constructor knobs
Three documentation consistency fixes from PR review.
OTelObserver and LangfuseObserver gained public
implementation_name / implementation_version constructor
parameters in the prior commit, but each class docstring's
"Constructor knobs" list was not updated. Adding entries to
both, slotted after spec_version (OTel) and after the trace
input/output hooks (Langfuse) so the always-emit knobs group
with the other invocation-span / trace-metadata sourcing
parameters.
The conformance.toml proposal 0052 comment block claimed both
observability/058 and /059 were deferred from the cross-
capability parser, but later in the same block stated 058
parses cleanly. Tightening the opening sentence so 058 is
correctly described as parseable-but-runtime-gated and 059 as
parser-deferred.
Copy file name to clipboardExpand all lines: CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,6 +16,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
16
16
17
17
### Added
18
18
19
+
- **Implementation attribution attributes** (proposal 0052, observability §5.1 + §8.4.1, spec v0.44.0). Every OTel invocation span now carries `openarmature.implementation.name` (`"openarmature-python"`) and `openarmature.implementation.version` (the package's `__version__`) alongside the existing `openarmature.graph.spec_version`. Every Langfuse Trace mirrors the rows as `trace.metadata.implementation_name` / `trace.metadata.implementation_version`. The values let operators triage in their observability backend without a separate deployment-manifest lookup: "which library, at which version, produced this trace" — the first question operators reach for. Always-emit invariant: neither `disable_state_payload`, `disable_llm_payload`, nor any other privacy knob gates these attributes, since they describe runtime identity rather than runtime data. Both observers expose `implementation_name` and `implementation_version` dataclass fields for test parameterization; the defaults read from the package identity via the same lazy-import pattern as `spec_version`. A new `openarmature.__implementation_name__ = "openarmature-python"` constant joins `__version__` and `__spec_version__` at the package root. The §3.4 reserved-key set grows from 24 to 26 names — `implementation_name` and `implementation_version` are reserved against caller-supplied collision, so a caller passing `invocation_metadata={"implementation_name": "spoof"}` is rejected at the `invoke()` boundary with `ValueError`.
19
20
- **`CompiledGraph.drain_events_for(invocation_id, *, timeout=5.0) -> DrainSummary`** (proposal 0054, spec graph-engine §6 *Per-invocation drain*, v0.46.0). The architectural pair to proposal 0048's §9.4 queryable observer accumulator lifecycle: a terminal node calling `await graph.drain_events_for(state.invocation_id)` blocks until every event dispatched for that invocation has reached every attached observer, typically followed by a read against a queryable observer accumulator whose bucket the drain has now caught up to. Snapshot semantic: the drain awaits the events dispatched as of call time; new emissions after the call are out of scope. Reuses the existing `DrainSummary` shape verbatim — no new `InvocationDrainSummary` variant. **Load-bearing divergence from `drain()`**: a per-invocation drain timeout MUST NOT cancel the delivery worker, in contrast to `drain()`'s shutdown semantics. The graph stays serving other invocations after the timeout fires; the deliver loop keeps processing the queue. Default timeout is `5.0` seconds; `None` waits indefinitely; `0.0` is a non-blocking check. Negative or `NaN` timeout raises `ValueError` at the API boundary. Unknown `invocation_id` (already drained or never started) returns an empty summary, not an error.
20
21
-**`get_invocation_metadata()` read-symmetric API** (proposal 0048, observability §3.4, spec v0.40.0). The canonical spec-idiomatic public name for the §3.4 read access pairs with `set_invocation_metadata()` on the write side: same function object as the historical `current_invocation_metadata`, exposed for callers wishing to use the symmetric `get_/set_` naming. Returns the `MappingProxyType` snapshot of the current async context's view (caller baseline + in-node augments), or the empty mapping outside any active invocation. Read-only — callers MUST NOT mutate it. Both names are now exported from `openarmature.observability`; existing `current_invocation_metadata` callers continue to work unchanged.
21
22
-**`docs/concepts/observability.md` §9 *Queryable observer pattern*** documents the convention-only observer-attached read methods that proposal 0048 §9 blesses: how to add a `get_*` read method to a custom observer (§9.1), the async-safety contract for concurrent reads under in-flight delivery (§9.2), the three-channel data-access guidance (typed State / untyped invocation metadata / queryable observer accumulator, §9.3, with a side-by-side table), and the lifecycle / explicit `drop(invocation_id)` discipline (§9.4). No new abstract surface on `Observer` per the spec — the pattern is convention-only and exists to bless the existing observer-state read shape used in production code.
0 commit comments