Skip to content

Add LlmCompletionEvent typed event variant#137

Merged
chris-colinsky merged 1 commit into
mainfrom
feature/0049-llm-completion-event
Jun 6, 2026
Merged

Add LlmCompletionEvent typed event variant#137
chris-colinsky merged 1 commit into
mainfrom
feature/0049-llm-completion-event

Conversation

@chris-colinsky

@chris-colinsky chris-colinsky commented Jun 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Defines LlmCompletionEvent as a new typed variant on the observer event union per proposal 0049. Carries the LLM call's identity, scoping, and outcome data as a structured record; observers filter via isinstance() rather than the impl-current sentinel-namespace string match. Reuses the existing openarmature.llm.response.Usage shape via a TYPE_CHECKING import + string forward reference to sidestep the runtime cycle through openarmature.llm.
  • OpenAI provider dual-emits during the v0.13.0 transition window: the existing sentinel NodeEvent pair continues firing for backwards-compat observability; the new typed event fires on the success path only (completion-only per spec v1 scope). Failure paths emit only the sentinel pair. Wall-clock latency measured at the adapter boundary with time.perf_counter().
  • New populate_caller_metadata: bool = False constructor kwarg on OpenAIProvider wires the opt-in caller-metadata snapshot per spec Q2 ack (option c — provider-side knob). Default off matches the spec's "default absent" semantics.
  • OTel and Langfuse observers gain an isinstance(event, LlmCompletionEvent) early-return to stay Protocol-compatible with the widened ObserverEvent union. Migration to actual type-discrimination consumption lands in the next PR; this PR keeps observer behavior unchanged.
  • Second PR of the v0.13.0 LLM hardening cycle (proposal 0049). Foundation for the OTel + Langfuse observer migration in PR 3.

Test plan

  • uv run pytest tests/ -q (1142 passing; 8 new tests in test_llm_provider.py covering dual-emit on success, no typed event on failure, outcome-field sourcing, opt-in default + on, request_id None case, arrival-order assertion, node-identity ContextVar sourcing)
  • uv run pyright src examples tests (clean over the whole tree)
  • uv run ruff check src examples tests (clean)
  • Eyeball src/openarmature/graph/events.py (the typed event field set + TYPE_CHECKING pattern for the Usage forward reference)
  • Eyeball src/openarmature/llm/providers/openai.py (the dispatch site around line 522 + the _build_llm_completion_event helper)

Notes

  • The LlmEventPayload sentinel pattern stays in place through v0.13.0. v0.15.0 will drop the sentinel emission per spec Q3 ack (CHANGELOG callout to pin in the release-prep PR).
  • The OTel + Langfuse observers' isinstance early-returns are deliberately minimal: they're protocol-compatibility fixes, not the migration. PR 3 swaps the sentinel-namespace handling for typed-event consumption.

Define LlmCompletionEvent as a new typed variant on the observer
event union per proposal 0049. Fields carry the LLM call's identity,
scoping, and outcome data as a structured record; observers filter
via isinstance() rather than the impl-current sentinel-namespace
string match. The Usage field reuses the existing
openarmature.llm.response.Usage class via a TYPE_CHECKING import
and string forward reference, sidestepping the runtime cycle
through openarmature.llm.

The OpenAI provider emits the typed event alongside the existing
sentinel NodeEvent pair during the dual-emit transition window per
spec section 5.5.7's SHOULD-emit-both framing. Failure paths emit
only the sentinel pair; the typed event is completion-only per the
proposal's v1 scope. Wall-clock latency is measured at the adapter
boundary with time.perf_counter().

The opt-in caller_invocation_metadata field is wired via a new
populate_caller_metadata constructor kwarg on OpenAIProvider per
spec Q2 ack (default False; snapshot via dict() when True).

The OTel and Langfuse observers gain an isinstance early-return
branch for LlmCompletionEvent to stay Protocol-compatible with the
widened ObserverEvent union. Migration to type-discrimination
filtering of the typed event lands in a subsequent PR; this PR
keeps observer behavior unchanged (they continue driving spans /
generations off the sentinel NodeEvent pair).

Foundation for the OTel and Langfuse observer migrations to
type-discrimination filtering in subsequent PRs of the v0.13.0
LLM hardening cycle.
Copilot AI review requested due to automatic review settings June 6, 2026 19:30
@chris-colinsky chris-colinsky merged commit 3307b5b into main Jun 6, 2026
7 checks passed
@chris-colinsky chris-colinsky deleted the feature/0049-llm-completion-event branch June 6, 2026 19:32

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new typed observer event variant (LlmCompletionEvent) to represent successful LLM completions (proposal 0049), and updates the OpenAI provider and observers to support a v0.13.0 dual-emit transition (legacy sentinel NodeEvent pair + new typed event).

Changes:

  • Introduces LlmCompletionEvent and widens the ObserverEvent union to include it.
  • Updates OpenAIProvider.complete() to dual-emit (success: sentinel pair + typed event; failure: sentinel pair only) and records adapter-boundary latency.
  • Updates OTel and Langfuse observers to remain Protocol-compatible by explicitly ignoring LlmCompletionEvent for now; adds unit tests covering dual-emit behavior and field sourcing.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/unit/test_llm_provider.py Adds tests asserting dual-emission, typed event field sourcing, opt-in caller metadata, ordering, and ContextVar scoping.
src/openarmature/observability/otel/observer.py Extends accepted event union and ignores LlmCompletionEvent during the transition window.
src/openarmature/observability/langfuse/observer.py Extends accepted event union and ignores LlmCompletionEvent during the transition window.
src/openarmature/observability/correlation.py Widens the active dispatch callable type to accept LlmCompletionEvent.
src/openarmature/llm/providers/openai.py Dual-emits the new typed event on success and measures adapter-boundary latency; adds provider opt-in knob for typed metadata snapshotting.
src/openarmature/graph/observer.py Widens ObserverEvent to include LlmCompletionEvent and documents its role.
src/openarmature/graph/events.py Defines the new LlmCompletionEvent dataclass (with a Usage forward reference to avoid import cycles).
src/openarmature/graph/init.py Re-exports LlmCompletionEvent from the public graph API.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +558 to +562
if self._populate_caller_metadata:
# Snapshot via dict() so downstream consumers see a stable
# frozen view; if a node body mutates metadata after the
# snapshot, the event still carries the at-emission view.
caller_metadata = dict(current_invocation_metadata())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants