Skip to content

openarmature.observability

openarmature.observability: cross-backend observability surface.

Two layers:

  • Core (this module + correlation.py): always available, no extra dependencies. Exposes :func:current_correlation_id and :func:current_active_observers; the ContextVar primitives that every backend mapping consumes.
  • Backend mappings (under observability.otel and future observability.langfuse etc.): gated behind optional dependencies (pip install openarmature[otel]). Importing the subpackage without the extras installed raises an informative ImportError pointing the caller at the install command.

At v1.0 launch the backend mappings will lift into sibling packages (openarmature-otel, openarmature-langfuse); until then they live here under per-backend subpackages so the layering is established up front.

LlmEventPayload

Bases: BaseModel

Typed payload carried on NodeEvent.pre_state for the openarmature.llm.complete event pair an LLM provider emits around each complete() call.

Observers subscribing to events with namespace :data:openarmature.observability.LLM_NAMESPACE read attributes directly off this payload. The OpenAI provider populates every field; third-party providers populate the subset they support.

ToolCallScope

ToolCallScope(call_id: str)

Handle yielded by :func:with_tool_call.

The caller reports the tool's return value via :meth:set_result so the success event can carry it. call_id is OA's per-execution correlation token (minted when the scope is entered), exposed for the caller to correlate a deferred completion if needed.

set_result

set_result(value: Any) -> None

Report the tool's return value to the scope.

current_active_observers

current_active_observers() -> (
    tuple[SubscribedObserver, ...]
)

Return the observer tuple in scope for the current node body (or empty tuple outside any invocation).

Capability code that needs to emit observer events from outside the engine's per-step machinery (e.g., the llm-provider span hook inside OpenAIProvider.complete) reads this to find which observers should receive the event. Combined with the engine's delivery queue, this preserves strict serial event ordering across all event sources within an invocation.

Returns an empty tuple when no invocation is active, by design; callers can iterate without a None check.

current_attempt_index

current_attempt_index() -> int

Return the attempt_index of the node currently executing, or 0 outside any node body. Retry middleware bumps this per attempt; the OTel observer uses it to disambiguate per-attempt spans when an LLM call happens inside a retried node body.

current_correlation_id

current_correlation_id() -> str | None

Return the correlation ID for the current invocation, or None if no openarmature invocation is in scope.

The correlation ID is readable from anywhere within an invocation's async call tree (node bodies, middleware, observers) without explicit threading through function arguments. This is the public reader.

Returns None outside an invocation (e.g., at module import time, inside a test that runs without going through invoke()). Callers MUST handle the None case rather than asserting a string is always present.

current_dispatch

current_dispatch() -> (
    Callable[
        [
            NodeEvent
            | MetadataAugmentationEvent
            | InvocationStartedEvent
            | InvocationCompletedEvent
            | LlmCompletionEvent
            | LlmFailedEvent
            | LlmRetryAttemptEvent
            | FailureIsolatedEvent
            | ToolCallEvent
            | ToolCallFailedEvent
        ],
        None,
    ]
    | None
)

Return the engine's dispatch callable for the current invocation, or None outside any invocation.

Capability code emitting observer events from inside a node body calls this to put a NodeEvent-shaped record (or a proposal- 0040 MetadataAugmentationEvent) on the engine's delivery queue. The queue's serial worker preserves per-invocation event ordering across all event sources (engine, checkpoint, LLM provider, mid-invocation metadata augmentation, future backends).

current_fan_out_index

current_fan_out_index() -> int | None

Return the fan_out_index of the node currently executing, or None outside any fan-out instance body (top-level nodes, subgraph dispatch, between nodes).

current_invocation_id

current_invocation_id() -> str | None

Return the engine-minted invocation ID for the current invocation, or None if no openarmature invocation is in scope.

Every invocation produces a unique UUIDv4 invocation_id, framework-generated, surfaced as the openarmature.invocation_id attribute on the invocation span + on every per-backend record. This is the public reader for backend mappings (OTel, future Langfuse) that need to populate that attribute.

current_namespace_prefix

current_namespace_prefix() -> tuple[str, ...]

Return the namespace prefix of the node currently executing, or the empty tuple outside any node body.

The empty-tuple default makes top-level (outside-invocation) and between-nodes (e.g., middleware bodies) calls fall back to invocation-level parenting cleanly.

current_invocation_metadata

current_invocation_metadata() -> (
    MappingProxyType[str, AttributeValue]
)

Return the caller-supplied invocation metadata in scope, or the empty mapping outside any invocation.

Observers and capability code (LLM provider span hook, Langfuse observer, OTel observer) read this to surface the mapping on backend-specific records. The returned mapping is read-only; callers MUST NOT mutate it. Use :func:set_invocation_metadata to add entries.

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.

set_invocation_metadata

set_invocation_metadata(**entries: AttributeValue) -> None

Merge entries into the current async context's invocation metadata. Additive: existing keys with the same names are overwritten; other keys are preserved.

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 the lineage with their open observations and apply observation.update(metadata=...) / span.set_attribute(...) in place. Spans already CLOSED at call time are NOT retroactively updated.

Raises :class:ValueError if any key violates the reserved- namespace rule or any value is not OTel-attribute-compatible.

Outside any active invocation, this still updates the ContextVar (a fresh per-context override), but the value will not be observed by any backend since no observer is in scope: :func:current_dispatch returns None and no augmentation event is emitted. The empty-invocation case is supported for 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, which returns an immutable snapshot of the current async context's view.

with_tool_call

with_tool_call(
    tool_name: str,
    arguments: Mapping[str, Any] | None = None,
    *,
    tool_call_id: str | None = None
) -> Iterator[ToolCallScope]

Instrument a tool execution inside a node body.

Wrap the caller's tool execution in this scope and report the result via :meth:ToolCallScope.set_result::

with with_tool_call("get_weather", {"city": "Paris"}, tool_call_id="call_abc") as scope:
    result = await get_weather(city="Paris")
    scope.set_result(result)

On clean exit a :class:~openarmature.graph.events.ToolCallEvent is dispatched carrying the reported result; on an exception a :class:~openarmature.graph.events.ToolCallFailedEvent is dispatched (with the exception's type + message) and the exception re-raises -- the scope observes, it does not swallow. OA does not run the tool, choose it, loop, or feed the result back to the model; those stay in the caller's graph.

arguments is the observability representation of the call inputs (for an LLM-originated call, the parsed ToolCall.arguments); it is independent of how the caller actually invokes the tool. tool_call_id links back to the LlmCompletionEvent.output_tool_calls entry this execution satisfies, or None for a standalone instrumented function. arguments and the result are payload; observer-side gating (disable_provider_payload) applies at rendering.