From b1cd394c2b66f5610f6dd73a339ee61fa0b6e7d1 Mon Sep 17 00:00:00 2001 From: chris-colinsky Date: Thu, 18 Jun 2026 08:30:37 -0700 Subject: [PATCH 01/15] Strip spec refs from observability docstrings --- src/openarmature/observability/correlation.py | 16 +-- .../observability/langfuse/__init__.py | 2 +- .../observability/langfuse/client.py | 17 +-- .../observability/langfuse/observer.py | 105 ++++++++++-------- src/openarmature/observability/metadata.py | 37 +++--- 5 files changed, 95 insertions(+), 82 deletions(-) diff --git a/src/openarmature/observability/correlation.py b/src/openarmature/observability/correlation.py index af515d7..bb8a434 100644 --- a/src/openarmature/observability/correlation.py +++ b/src/openarmature/observability/correlation.py @@ -139,14 +139,16 @@ def _reset_invocation_id(token: Token[str | None]) -> None: def validate_invocation_id(value: object) -> str: """Validate a caller-supplied ``invocation_id`` and return it. - Per observability §5.1 a caller-supplied id MAY be any non-empty - URL-safe string. Rejects empty / non-string / non-URL-safe values - at the ``invoke()`` boundary so the violation surfaces - synchronously to the caller rather than as a downstream trace-id - derivation failure. Typed ``object`` (like - :func:`validate_invocation_metadata`) so the boundary check guards - against untyped callers. Raises :class:`ValueError`. + A caller-supplied id MAY be any non-empty URL-safe string. Rejects + empty / non-string / non-URL-safe values at the ``invoke()`` + boundary so the violation surfaces synchronously to the caller + rather than as a downstream trace-id derivation failure. Typed + ``object`` (like :func:`validate_invocation_metadata`) so the + boundary check guards against untyped callers. Raises + :class:`ValueError`. """ + # Spec observability §5.1: a caller-supplied invocation_id MAY be + # any non-empty URL-safe string. if not isinstance(value, str): raise ValueError(f"invocation_id must be a string; got {type(value).__name__}") if not value: diff --git a/src/openarmature/observability/langfuse/__init__.py b/src/openarmature/observability/langfuse/__init__.py index 5657655..92e35c3 100644 --- a/src/openarmature/observability/langfuse/__init__.py +++ b/src/openarmature/observability/langfuse/__init__.py @@ -16,7 +16,7 @@ Public surface: - :class:`LangfuseObserver` — observer-driven Langfuse Trace + - Observation emission per spec observability §8. + Observation emission. - :class:`LangfuseClient` — Protocol the observer calls. Satisfied by the bundled :class:`InMemoryLangfuseClient` and (structurally) by the real ``langfuse.Langfuse`` SDK class. diff --git a/src/openarmature/observability/langfuse/client.py b/src/openarmature/observability/langfuse/client.py index 8091b76..276498b 100644 --- a/src/openarmature/observability/langfuse/client.py +++ b/src/openarmature/observability/langfuse/client.py @@ -11,7 +11,7 @@ """Langfuse client Protocol + in-memory recorder. -The :class:`LangfuseObserver` consumes the §6 OA event stream and +The :class:`LangfuseObserver` consumes the OA event stream and emits Langfuse Trace + Observation entities through a :class:`LangfuseClient`. The Protocol is intentionally narrow: it declares only the methods the observer calls. Concrete sinks: @@ -55,7 +55,7 @@ class LangfuseObservation: Carries the observation's type-discriminated shape — Spans hold timing + metadata; Generations add model/parameters/usage/input/ output/prompt-entity link; Events are point-in-time markers - (reserved per spec §8.2 — not used by this version of the mapping). + (reserved, not used by this version of the mapping). """ id: str @@ -161,7 +161,7 @@ class LangfuseClient(Protocol): closes. The Protocol does NOT define `event(...)` — Event observations - are reserved by §8.2 but not used in v0.23.0 of the mapping. + are reserved but not used by this mapping. """ def trace( @@ -173,10 +173,11 @@ def trace( ) -> None: """Create a new Trace. - The Trace `id` MUST be the OA invocation_id verbatim (§8.4.1). + The Trace `id` MUST be the OA invocation_id verbatim. Implementations track Traces internally; observation calls pass `trace_id` to associate. """ + # Spec §8.4.1: the Trace id is the OA invocation_id verbatim. ... # The current observer doesn't invoke this method — it sets the @@ -199,10 +200,10 @@ def update_trace( """Update an existing Trace's mutable fields after creation. Used by the observer when the caller-supplied invocation - label (§8.6) lands later than the Trace's open call, when - additional metadata becomes available mid-invocation, or - when the proposal 0043 invocation-boundary events populate - ``trace.input`` / ``trace.output``. + label lands later than the Trace's open call, when additional + metadata becomes available mid-invocation, or when the + invocation-boundary events populate ``trace.input`` / + ``trace.output``. """ ... diff --git a/src/openarmature/observability/langfuse/observer.py b/src/openarmature/observability/langfuse/observer.py index 384fc6c..61f0de5 100644 --- a/src/openarmature/observability/langfuse/observer.py +++ b/src/openarmature/observability/langfuse/observer.py @@ -97,10 +97,10 @@ def _read_implementation_version() -> str: class _OpenObservation: """An in-flight Langfuse observation pinned in the observer's state. - Per proposal 0045: carries the observation's own - ``fan_out_index_chain`` and ``branch_name_chain`` so the - augmentation walk can apply §3.4's lineage-aware boundary rule - (mirror of the OTel observer's ``_OpenSpan``).""" + Carries the observation's own ``fan_out_index_chain`` and + ``branch_name_chain`` so the augmentation walk can apply the + lineage-aware boundary rule (mirror of the OTel observer's + ``_OpenSpan``).""" handle: LangfuseSpanHandle | LangfuseGenerationHandle fan_out_index_chain: tuple[int | None, ...] = () @@ -138,22 +138,23 @@ def _empty_str_frozenset() -> frozenset[str]: def _apply_caller_metadata(metadata: dict[str, Any], caller_metadata: Mapping[str, Any]) -> None: """Merge caller-supplied invocation metadata into a Trace's or - Observation's metadata bag at top level per observability §8.4.1 - + §8.4.2 (proposal 0034). + Observation's metadata bag at top level. - Top-level placement is by spec: Langfuse UI filters on + Top-level placement lets the Langfuse UI filter on ``metadata.`` directly, so caller-supplied entries become siblings to ``correlation_id`` / ``entry_node`` rather than nested under a ``user`` sub-object. - Reserved-key collision with §8.4.1 / §8.4.2 keys + Reserved-key collision with the OA-emitted keys (``correlation_id``, ``entry_node``, ``spec_version``, - ``namespace``, etc.) is not currently checked here: the spec - permits the rejection to happen at either boundary, and the - ``invoke()`` API-boundary validation already rejects - ``openarmature.*`` / ``gen_ai.*`` prefixed keys. Per-Langfuse- - backend collision rejection is queued as a follow-up. + ``namespace``, etc.) is not currently checked here: the rejection + may happen at either boundary, and the ``invoke()`` API-boundary + validation already rejects ``openarmature.*`` / ``gen_ai.*`` + prefixed keys. Per-Langfuse-backend collision rejection is queued + as a follow-up. """ + # Spec observability §8.4.1 / §8.4.2 (proposal 0034): top-level + # placement of caller-supplied metadata on the Trace / Observation. for key, value in caller_metadata.items(): metadata[key] = value @@ -163,15 +164,16 @@ def _subgraph_identity_at(event: NodeEvent, depth: int) -> str: given 1-based namespace depth, or the empty string when no identity is tracked at that depth. - Per observability §5.3 + the coord-thread - ``clarify-subgraph-name-semantics`` resolution: the empty-string - fallback matches the spec's "if the implementation tracks one" - clause for implementations / direct ``SubgraphNode(...)`` callers - that don't wire an identity through. Conformance fixtures - 031/032/033 lock identity as the required value; the empty-string - path keeps direct callers conformant with §5.3 but failing those - fixtures. + The empty-string fallback matches the spec's "if the + implementation tracks one" clause for implementations / direct + ``SubgraphNode(...)`` callers that don't wire an identity through. + Conformance fixtures 031/032/033 lock identity as the required + value; the empty-string path keeps direct callers conformant but + failing those fixtures. """ + # Spec observability §5.3 (coord thread + # clarify-subgraph-name-semantics): empty-string fallback is + # conformant for callers that don't track a subgraph identity. idx = depth - 1 if 0 <= idx < len(event.subgraph_identities): identity = event.subgraph_identities[idx] @@ -254,12 +256,12 @@ class _InvState: @dataclass class LangfuseObserver: - """Observer-driven Langfuse mapping per spec observability §8. + """Observer-driven Langfuse mapping. Construct with a :class:`LangfuseClient` — the bundled :class:`InMemoryLangfuseClient` for tests, or a real ``langfuse.Langfuse()`` instance for production. The observer - handles the §6 event stream and emits Trace + Observation entities + handles the event stream and emits Trace + Observation entities through the client. Constructor knobs: @@ -267,34 +269,34 @@ class LangfuseObserver: - ``client``: the Langfuse sink (Protocol-typed). - ``disable_llm_spans``: when ``True`` the observer skips Generation observations on LLM provider events. - - ``disable_provider_payload``: default ``True`` per §8.9's "symmetric - privacy posture" with the OTel observer. Gates + - ``disable_provider_payload``: default ``True`` for a symmetric + privacy posture with the OTel observer. Gates ``generation.input`` / ``output`` / ``metadata.request_extras`` emission. The name carries the broadened provider-payload scope; LLM completion is OA's only provider-call payload today. - ``payload_byte_cap``: per-attribute byte cap on the source payload string before parse-back. Mirrors the OTel observer's ``payload_max_bytes`` semantic — emission preserves the raw - truncated string when the §5.5.5 marker is present (per §8.7). - Default 64 KiB; same minimum (256 bytes) applies. + truncated string when the truncation marker is present. Default + 64 KiB; same minimum (256 bytes) applies. - ``detached_subgraphs``: set of subgraph wrapper node names that - run in their own Langfuse Trace per §8.5. Each such subgraph - gets a fresh trace_id; the main Trace's dispatch observation - surfaces the link via ``metadata.detached_child_trace_ids``. + run in their own Langfuse Trace. Each such subgraph gets a fresh + trace_id; the main Trace's dispatch observation surfaces the link + via ``metadata.detached_child_trace_ids``. - ``detached_fan_outs``: set of fan-out node names whose instances each get their own Langfuse Trace. Same link mechanism on the fan-out node observation: each per-instance detached trace_id lands in the array. - - ``disable_state_payload``: default ``True`` per §8.4.1 *Trace - input/output sourcing* (proposal 0043). When ``True`` the - observer does NOT serialize ``initial_state`` / final state - directly onto ``trace.input`` / ``trace.output``; the minimal - stub applies unless ``trace_input_from_state`` / - ``trace_output_from_state`` overrides. When ``False`` the raw - state object is serialized to the Trace fields, subject to - ``payload_byte_cap`` truncation. Independent of - ``disable_provider_payload`` — the two payloads carry distinct - threat models (LLM-call transcript vs. application state). + - ``disable_state_payload``: default ``True`` (Trace input/output + sourcing). When ``True`` the observer does NOT serialize + ``initial_state`` / final state directly onto ``trace.input`` / + ``trace.output``; the minimal stub applies unless + ``trace_input_from_state`` / ``trace_output_from_state`` + overrides. When ``False`` the raw state object is serialized to + the Trace fields, subject to ``payload_byte_cap`` truncation. + Independent of ``disable_provider_payload`` — the two payloads + carry distinct threat models (LLM-call transcript vs. + application state). - ``trace_input_from_state``: optional caller hook returning the value to use as ``trace.input``. Called once per invocation at the ``InvocationStartedEvent``. Returning ``None`` falls @@ -309,8 +311,8 @@ class LangfuseObserver: parameterization. - ``implementation_version``: string surfaced as ``trace.metadata.implementation_version`` on every Trace. - Defaults to ``openarmature.__version__``. Always-emit invariant - inherited from §5.1 — not gated by ``disable_state_payload``, + Defaults to ``openarmature.__version__``. Always emitted — + not gated by ``disable_state_payload``, ``disable_provider_payload``, or any other privacy knob. The observer reads the spec version from the package at @@ -319,6 +321,11 @@ class LangfuseObserver: state isolation keys all internal maps by invocation_id. """ + # Spec observability §8 (Langfuse backend mapping). Knob spec + # basis: §8.9 privacy posture; §8.4.1 Trace input/output sourcing + # (proposal 0043); §8.5 detached traces; §5.1 always-emit + # attribution invariant. + client: LangfuseClient disable_llm_spans: bool = False disable_provider_payload: bool = True @@ -1451,9 +1458,9 @@ def _handle_typed_llm_completion(self, event: LlmCompletionEvent) -> None: def _handle_typed_llm_failed(self, event: LlmFailedEvent) -> None: """Open + close an ERROR-level Generation observation from the - typed LlmFailedEvent (failure path, proposal 0058). Same shape - as the success path with ERROR level + error_category as the - Generation observation's statusMessage.""" + typed LlmFailedEvent (failure path). Same shape as the success + path with ERROR level + error_category as the Generation + observation's statusMessage.""" from openarmature.observability.correlation import ( current_correlation_id, current_invocation_id, @@ -1600,8 +1607,9 @@ def _typed_event_metadata( return metadata def _usage_from_typed_event(self, event: LlmCompletionEvent) -> LangfuseUsage | None: - """Map the typed event's Usage onto the Langfuse Usage record - per §8.4.3. Returns None when no usage was reported.""" + """Map the typed event's Usage onto the Langfuse Usage record. + Returns None when no usage was reported.""" + # Spec observability §8.4.3 (Langfuse usage mapping). usage = event.usage if usage is None: return None @@ -1614,8 +1622,9 @@ def _usage_from_typed_event(self, event: LlmCompletionEvent) -> LangfuseUsage | ) def _resolve_prompt_link_from_typed_event(self, event: LlmCompletionEvent | LlmFailedEvent) -> Any: - """§8.4.4 case discrimination on the typed event's active_prompt + """Case discrimination on the typed event's active_prompt snapshot.""" + # Spec observability §8.4.4. active_prompt = event.active_prompt if active_prompt is None: return None diff --git a/src/openarmature/observability/metadata.py b/src/openarmature/observability/metadata.py index e2c381b..10f775d 100644 --- a/src/openarmature/observability/metadata.py +++ b/src/openarmature/observability/metadata.py @@ -7,7 +7,7 @@ # at the ``invoke()`` boundary and at mid-invocation augmentation # via ``set_invocation_metadata``. -"""Caller-supplied invocation metadata (proposal 0034). +"""Caller-supplied invocation metadata. Two surfaces: @@ -30,8 +30,8 @@ for spec-normative attribute namespaces; collisions would silently overwrite OA-emitted state at the observer layer). - Keys MUST NOT exactly match a reserved OA-emitted top-level metadata - key name (the §8.4 Langfuse set plus ``invocation_id``; proposal - 0041) for the same collision reason. + key name (the Langfuse set plus ``invocation_id``) for the same + collision reason. - Values MUST be OTel-attribute-compatible scalars: ``str``, ``int``, ``float``, ``bool``, or a homogeneous list/tuple of those types. ``None``, nested objects, and mixed-type arrays are rejected. @@ -123,11 +123,10 @@ def current_invocation_metadata() -> MappingProxyType[str, AttributeValue]: callers MUST NOT mutate it. Use :func:`set_invocation_metadata` to add entries. - Aliased as :func:`get_invocation_metadata` per spec §3.4 (proposal - 0048, v0.40.0); the alias is the canonical spec-idiomatic name - paralleling :func:`set_invocation_metadata`. Both names point at - the same function — pick whichever reads naturally at the call - site. + Aliased as :func:`get_invocation_metadata`; the alias is the + canonical idiomatic name paralleling :func:`set_invocation_metadata`. + Both names point at the same function — pick whichever reads + naturally at the call site. """ return _invocation_metadata_var.get() @@ -146,10 +145,10 @@ def set_invocation_metadata(**entries: AttributeValue) -> None: metadata. Additive: existing keys with the same names are overwritten; other keys are preserved. - Per spec §3.4: affects spans / observations emitted AFTER the - call returns. Open observations whose lineage covers the calling - context ARE updated in place per proposal 0040 — implementations - enqueue a :class:`~openarmature.graph.events.MetadataAugmentationEvent` + Affects spans / observations emitted AFTER the call returns. Open + observations whose lineage covers the calling context ARE updated + in place: implementations enqueue a + :class:`~openarmature.graph.events.MetadataAugmentationEvent` on the engine's serial observer-delivery queue carrying the delta + the calling context's lineage tuple (namespace, attempt_index, fan_out_index, branch_name); observers correlate @@ -169,10 +168,11 @@ def set_invocation_metadata(**entries: AttributeValue) -> None: symmetry; users typically call this from inside a node body, middleware, or observer where an invocation is already in flight. - Symmetric with :func:`get_invocation_metadata` (proposal 0048, - spec §3.4 v0.40.0) which returns an immutable snapshot of the - current async context's view. + Symmetric with :func:`get_invocation_metadata`, which returns an + immutable snapshot of the current async context's view. """ + # Spec observability §3.4: additive merge, affecting only spans / + # observations emitted after this call returns. if not entries: return for key, value in entries.items(): @@ -226,13 +226,14 @@ def validate_invocation_metadata(mapping: object) -> MappingProxyType[str, Attri read-only view the engine stashes on the ContextVar. Public so the engine (`CompiledGraph.invoke`) calls this at the - boundary BEFORE any work begins; per spec §3.4 the rejection - surfaces as a synchronous error to the caller of ``invoke()`` - rather than as a backend-emission failure. + boundary BEFORE any work begins; the rejection surfaces as a + synchronous error to the caller of ``invoke()`` rather than as a + backend-emission failure. Returns the validated read-only mapping. Raises :class:`ValueError` on any rule violation (with a message naming the offending key). """ + # Spec observability §3.4: boundary validation, synchronous rejection. if mapping is None: return _EMPTY_METADATA if not isinstance(mapping, dict): From 113eb3cfb9aa4ee03b1c0ec625725d139dbbc5b8 Mon Sep 17 00:00:00 2001 From: chris-colinsky Date: Thu, 18 Jun 2026 10:29:32 -0700 Subject: [PATCH 02/15] Strip spec refs from otel observer docstrings --- .../observability/otel/observer.py | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/openarmature/observability/otel/observer.py b/src/openarmature/observability/otel/observer.py index 39c3832..b1b6b20 100644 --- a/src/openarmature/observability/otel/observer.py +++ b/src/openarmature/observability/otel/observer.py @@ -166,8 +166,7 @@ def _read_implementation_version() -> str: def _apply_caller_metadata(attrs: dict[str, Any], metadata: Mapping[str, Any]) -> None: """Merge caller-supplied invocation metadata into a span's - attribute dict as ``openarmature.user.`` entries per - observability §5.6. + attribute dict as ``openarmature.user.`` entries. Called at every span-emission site so the metadata family is cross-cutting (invocation span, every node span, subgraph @@ -187,12 +186,13 @@ def _subgraph_identity_at(event: NodeEvent, depth: int) -> str: given 1-based namespace depth, or the empty string when no identity is tracked at that depth. - Per observability §5.3 + the coord-thread - ``clarify-subgraph-name-semantics`` resolution: empty-string - fallback matches the spec's "if the implementation tracks one" - clause for callers using ``SubgraphNode(name=..., compiled=...)`` - without supplying ``subgraph_identity``. + The empty-string fallback matches the spec's "if the implementation + tracks one" clause for callers using + ``SubgraphNode(name=..., compiled=...)`` without supplying + ``subgraph_identity``. """ + # Spec observability §5.3 (coord thread + # clarify-subgraph-name-semantics). idx = depth - 1 if 0 <= idx < len(event.subgraph_identities): identity = event.subgraph_identities[idx] @@ -214,10 +214,10 @@ class _OpenSpan: single event handler's scope, so no token needs to live across events. - Per proposal 0045: carries the span's own ``fan_out_index_chain`` - and ``branch_name_chain`` so the augmentation walk can apply - §3.4's lineage-aware boundary rule without re-deriving the chain - from successive events.""" + Carries the span's own ``fan_out_index_chain`` and + ``branch_name_chain`` so the augmentation walk can apply the + lineage-aware boundary rule without re-deriving the chain from + successive events.""" span: Span fan_out_index_chain: tuple[int | None, ...] = () @@ -231,7 +231,7 @@ def _span_chain_on_path( ) -> bool: """Return True iff ``open_span``'s chain is a prefix-match of the augmenter's chain — i.e., the span sits on the augmenter's - call-stack ancestor path. Per proposal 0045 §3.4: + call-stack ancestor path: - A span shorter than the augmenter (chain prefix-matches) is an ancestor on the path. @@ -240,6 +240,7 @@ def _span_chain_on_path( - A span deeper than the augmenter, OR with a position-mismatch anywhere, is a sibling and MUST NOT be updated. """ + # Spec observability §3.4 (proposal 0045): lineage-aware boundary. span_fi = open_span.fan_out_index_chain span_bn = open_span.branch_name_chain if len(span_fi) > len(aug_fi_chain): @@ -385,7 +386,7 @@ class _InvState: @dataclass class OTelObserver: - """Observer-driven OTel span lifecycle per spec observability §6. + """Observer-driven OTel span lifecycle. Construct with a :class:`SpanProcessor` (typically a :class:`BatchSpanProcessor` wrapping a real exporter, or a @@ -443,6 +444,7 @@ class OTelObserver: event handler's scope. """ + # Spec observability §6 (observer-driven span lifecycle). # span_processor accepts a single processor or a sequence per # observability friction-roundup #5. The dataclass field type is # the union; ``__post_init__`` normalizes to a tuple internally. @@ -764,7 +766,7 @@ def _open_started_span(self, event: NodeEvent) -> None: ) def _handle_completed(self, event: NodeEvent) -> None: - """Close the matching span, applying §4.2 status mapping.""" + """Close the matching span, applying the status mapping.""" from openarmature.observability.correlation import current_invocation_id invocation_id = current_invocation_id() @@ -942,8 +944,7 @@ def _collect_augmentation_targets( self, invocation_id: str, event: MetadataAugmentationEvent ) -> list[Span]: """Collect open spans on the augmenter's call-stack ancestor - chain per proposal 0045 §3.4. Three-step boundary decision - tree per open span: + chain. Three-step boundary decision tree per open span: 1. Same context as augmenter (or descendant sharing the mutated mapping) — update. @@ -1060,17 +1061,17 @@ def _collect_augmentation_targets( # ------------------------------------------------------------------ def _emit_checkpoint_migrate_span(self, event: NodeEvent) -> None: - """Spec pipeline-utilities §6 cross-ref (proposal 0014): emit a - zero-duration ``openarmature.checkpoint.migrate`` span when - a versioned resume's migration chain runs. The synthetic - event carries ``_MigrationSummary`` on ``pre_state``; this - handler reads ``from_version`` / ``to_version`` / + """Emit a zero-duration ``openarmature.checkpoint.migrate`` + span when a versioned resume's migration chain runs. The + synthetic event carries ``_MigrationSummary`` on ``pre_state``; + this handler reads ``from_version`` / ``to_version`` / ``chain_length`` from the summary onto the span. Emitted under the invocation's root span (no parent-node context — the migration runs before any node fires), so trace UIs surface it as the first child of the invocation. """ + # Spec pipeline-utilities §6 cross-ref (proposal 0014). from openarmature.graph.compiled import _MigrationSummary from openarmature.observability.correlation import ( current_correlation_id, @@ -1117,10 +1118,10 @@ def _emit_checkpoint_migrate_span(self, event: NodeEvent) -> None: span.end() def _emit_checkpoint_save_span(self, event: NodeEvent) -> None: - """Spec pipeline-utilities §10.8 + observability §4.5: emit a - zero-duration ``openarmature.checkpoint.save`` span attached - to the most-recently-opened node span (the node whose + """Emit a zero-duration ``openarmature.checkpoint.save`` span + attached to the most-recently-opened node span (the node whose completed event triggered the save).""" + # Spec pipeline-utilities §10.8 + observability §4.5. from openarmature.observability.correlation import ( current_correlation_id, current_invocation_id, @@ -1313,8 +1314,8 @@ def _handle_typed_llm_completion(self, event: LlmCompletionEvent) -> None: def _handle_typed_llm_failed(self, event: LlmFailedEvent) -> None: """Open + close the ``openarmature.llm.complete`` span from the - typed LlmFailedEvent (failure path, proposal 0058). Same span - shape as the success path with ERROR status + + typed LlmFailedEvent (failure path). Same span shape as the + success path with ERROR status + ``openarmature.error.category`` attribute attached.""" from openarmature.observability.correlation import ( current_correlation_id, @@ -1605,16 +1606,17 @@ def _sync_subgraph_spans( correlation_id: str | None, event: NodeEvent, ) -> None: - """Open any synthetic subgraph dispatch spans we need (per - observability §4.5: subgraph wrapper MUST emit a span); close - any subgraph spans whose prefix is no longer an ancestor of - the current event's namespace. + """Open any synthetic subgraph dispatch spans we need (the + subgraph wrapper MUST emit a span); close any subgraph spans + whose prefix is no longer an ancestor of the current event's + namespace. Called from ``_open_started_span`` BEFORE opening the leaf node span. Detached-mode entries (subgraph or fan-out instance) are registered as detached roots so their inner spans live in a fresh trace. """ + # Spec observability §4.5: the subgraph wrapper emits a span. namespace = event.namespace # 1. Close any open subgraph spans that aren't ancestors of # the current namespace — we've left those subgraphs. @@ -2015,10 +2017,9 @@ def _open_fan_out_instance_dispatch_span( prefix: tuple[str, ...], event: NodeEvent, ) -> None: - """Per-instance dispatch span for a non-detached fan-out - (per spec §5.4 + proposal 0013, v0.10.0). Mirror of - ``_open_detached_fan_out_instance_root`` but lives in the - parent trace (no fresh trace_id). + """Per-instance dispatch span for a non-detached fan-out. + Mirror of ``_open_detached_fan_out_instance_root`` but lives in + the parent trace (no fresh trace_id). Parents under the fan-out node span at ``prefix``. Span name is the fan-out node's name; attributes are @@ -2084,9 +2085,8 @@ def _open_parallel_branches_branch_dispatch_span( prefix: tuple[str, ...], event: NodeEvent, ) -> None: - """Per-branch dispatch span for a parallel-branches NODE (per - observability §5.7 + proposal 0044, v0.36.0). Mirror of - ``_open_fan_out_instance_dispatch_span``. + """Per-branch dispatch span for a parallel-branches NODE. + Mirror of ``_open_fan_out_instance_dispatch_span``. Parents under the parallel-branches node span at ``prefix``. Span name is the branch's identifier (``event.branch_name``). @@ -2209,7 +2209,7 @@ def _find_fan_out_node_span(self, inv_state: _InvState, prefix: tuple[str, ...]) return None def _node_attrs(self, event: NodeEvent, correlation_id: str | None) -> dict[str, Any]: - """Build the §5 attribute set for a node span.""" + """Build the attribute set for a node span.""" attrs: dict[str, Any] = { "openarmature.node.name": event.node_name, "openarmature.node.namespace": list(event.namespace), From 311c23f2b26f9e0072021936060520a95e0d3e17 Mon Sep 17 00:00:00 2001 From: chris-colinsky Date: Thu, 18 Jun 2026 10:32:49 -0700 Subject: [PATCH 03/15] Strip spec refs from llm docstrings --- src/openarmature/llm/messages.py | 8 +++---- src/openarmature/llm/provider.py | 17 ++++++++------- src/openarmature/llm/providers/openai.py | 27 ++++++++++++------------ 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/openarmature/llm/messages.py b/src/openarmature/llm/messages.py index 9bc32a8..f52ba2e 100644 --- a/src/openarmature/llm/messages.py +++ b/src/openarmature/llm/messages.py @@ -77,10 +77,10 @@ class Tool(BaseModel): class ForceTool(BaseModel): """Force the model to call exactly the named tool. - Use the record form of the §5 `tool_choice` discriminated union - when you need the model to call a specific tool by name. ``type`` - is the spec-level discriminator (``"tool"``); the wire mapping - (§8.1.1) renames it to ``"function"`` for the OpenAI body. The + Use the record form of the `tool_choice` discriminated union when + you need the model to call a specific tool by name. ``type`` is the + spec-level discriminator (``"tool"``); the wire mapping renames it + to ``"function"`` for the OpenAI body. The ``name`` MUST match a ``Tool.name`` in the supplied ``tools`` list; ``validate_tool_choice`` enforces this at pre-send time and raises ``ProviderInvalidRequest`` on violation. diff --git a/src/openarmature/llm/provider.py b/src/openarmature/llm/provider.py index 71e03ac..59ae808 100644 --- a/src/openarmature/llm/provider.py +++ b/src/openarmature/llm/provider.py @@ -100,8 +100,8 @@ async def complete( supplied, the implementation constrains the model's output to the schema and populates ``Response.parsed`` with the validated value. - tool_choice: Optional tool-choice constraint (spec §5). One - of ``"auto"``, ``"required"``, ``"none"``, or a + tool_choice: Optional tool-choice constraint. One of + ``"auto"``, ``"required"``, ``"none"``, or a :class:`ForceTool` record. When ``None`` (the default) the wire ``tool_choice`` field is omitted and the provider's own default applies. Pre-send validation @@ -213,9 +213,9 @@ def validate_tool_choice( tool_choice: ToolChoice | None, tools: Sequence[Tool] | None, ) -> None: - """Validate ``tool_choice`` against ``tools`` per spec §5. + """Validate ``tool_choice`` against ``tools``. - Raises :class:`ProviderInvalidRequest` (the §7 + Raises :class:`ProviderInvalidRequest` (the ``provider_invalid_request`` category) on: - ``tool_choice`` supplied as a string that is not one of @@ -229,11 +229,12 @@ def validate_tool_choice( - ``tool_choice=ForceTool(name=X)`` supplied with ``X`` not in the supplied tools list. - No-op when ``tool_choice`` is ``None`` (the default — preserves - pre-0025 behavior; the wire field is omitted and the provider's - own default applies). ``tool_choice="auto"`` and - ``tool_choice="none"`` have no ``tools``-related preconditions. + No-op when ``tool_choice`` is ``None`` (the default — the wire + field is omitted and the provider's own default applies). + ``tool_choice="auto"`` and ``tool_choice="none"`` have no + ``tools``-related preconditions. """ + # Spec llm-provider §5 (tool_choice) / §7 (provider_invalid_request). if tool_choice is None: return # Two-layer type defense at the API boundary. Pyright catches the diff --git a/src/openarmature/llm/providers/openai.py b/src/openarmature/llm/providers/openai.py index b1551fb..8c86cb4 100644 --- a/src/openarmature/llm/providers/openai.py +++ b/src/openarmature/llm/providers/openai.py @@ -370,7 +370,7 @@ async def complete( class; when supplied as a JSON Schema dict, ``Response.parsed`` is the deserialized dict. - ``tool_choice`` is validated against ``tools`` per spec §5: + ``tool_choice`` is validated against ``tools``: ``"required"`` and the ``ForceTool`` record both demand non-empty ``tools``, and ``ForceTool.name`` must appear in the supplied list. Violations raise ``provider_invalid_request`` @@ -589,7 +589,7 @@ def _build_llm_completion_event( Sources identity / scoping fields from the calling-node ContextVars and outcome fields from the response. Request-side - fields (per proposal 0057) are passed through from the + fields are passed through from the provider's complete() local state — serialized message list, the gen_ai.request.* parameter mapping, the RuntimeConfig extras, the prompt-context snapshots taken at dispatch time, @@ -669,17 +669,17 @@ def _build_llm_failed_event( """Construct the typed LlmFailedEvent for the failure path. Sources identity / scoping fields from the calling-node - ContextVars and failure fields from the raised §7 exception. + ContextVars and failure fields from the raised exception. Field set mirrors LlmCompletionEvent (identity + request-side) - plus the three failure-specific fields per proposal 0058. + plus the three failure-specific fields. ``error_type`` defaults to the exception class name — falls into the "upstream exception class name" style documented in - the spec field table. Providers that have a vendor error code + the field table. Providers that have a vendor error code available (e.g. ``rate_limit_exceeded`` for OpenAI) can - override with vendor-specific detail in a future spec - proposal; for now the class name is the safest default since - every LlmProviderError subclass carries one. + override with vendor-specific detail in a future proposal; for + now the class name is the safest default since every + LlmProviderError subclass carries one. """ namespace = current_namespace_prefix() @@ -1178,7 +1178,8 @@ def _augment_messages_with_schema_directive( def _message_to_wire(msg: Message) -> dict[str, Any]: - """Spec §8.1.1 request mapping for one message.""" + """Request mapping for one message.""" + # Spec llm-provider §8.1.1. if isinstance(msg, SystemMessage): return {"role": "system", "content": msg.content} if isinstance(msg, UserMessage): @@ -1299,14 +1300,14 @@ def _tool_to_wire(tool: Tool) -> dict[str, Any]: def _wire_to_assistant_message(wire: dict[str, Any], *, lenient_args: bool) -> AssistantMessage: - """Parse OpenAI-shaped assistant message into spec §3 form. + """Parse an OpenAI-shaped assistant message into canonical form. When ``lenient_args=True`` (i.e. ``finish_reason == "error"``), tool calls with unparseable JSON arguments populate - ``arguments=None`` instead of raising. Per spec §3 "Validation - under finish_reason: error" — degraded responses surface what - they can; repair is a caller concern. + ``arguments=None`` instead of raising — degraded responses surface + what they can; repair is a caller concern. """ + # Spec llm-provider §3: validation under finish_reason "error". content_raw = wire.get("content") or "" content: str = content_raw if isinstance(content_raw, str) else "" raw_tool_calls = cast("list[Any]", wire.get("tool_calls") or []) From 1375daca6ddf85c666dfb2e75492631fb10f0653 Mon Sep 17 00:00:00 2001 From: chris-colinsky Date: Thu, 18 Jun 2026 10:38:01 -0700 Subject: [PATCH 04/15] Strip spec refs from prompts docstrings --- .../prompts/backends/filesystem.py | 4 +- src/openarmature/prompts/backends/langfuse.py | 13 ++--- src/openarmature/prompts/context.py | 15 +++--- src/openarmature/prompts/errors.py | 4 +- src/openarmature/prompts/hashing.py | 2 +- src/openarmature/prompts/label_resolver.py | 8 +-- src/openarmature/prompts/manager.py | 34 +++++++------ src/openarmature/prompts/prompt.py | 49 +++++++++---------- 8 files changed, 66 insertions(+), 63 deletions(-) diff --git a/src/openarmature/prompts/backends/filesystem.py b/src/openarmature/prompts/backends/filesystem.py index 0e0ef48..6b555a4 100644 --- a/src/openarmature/prompts/backends/filesystem.py +++ b/src/openarmature/prompts/backends/filesystem.py @@ -21,7 +21,7 @@ class FilesystemPromptBackend: - ``layout="per-label"`` (default): ``/