Skip to content

Commit cf76c53

Browse files
Extend LlmCompletionEvent with proposal 0057 fields (#141)
Bump spec pin to v0.51.0 and extend the typed LLM completion event with request-side fields per accepted proposal 0057: input_messages, output_content, request_params, request_extras, active_prompt, active_prompt_group, call_id, and response_model. Rename request_id to response_id to align with the proposal's response-side naming. OpenAI provider now populates the new fields at emission. Image bytes in input_messages stay redacted via the existing serializer. Observers (OTel, Langfuse) and fixtures 060-068 land in follow-up PRs in this cycle; conformance.toml marks 0057 implemented since the typed event contract is satisfied.
1 parent 644af66 commit cf76c53

11 files changed

Lines changed: 457 additions & 25 deletions

File tree

conformance.toml

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
[manifest]
3434
implementation = "openarmature-python"
35-
spec_pin = "v0.46.0"
35+
spec_pin = "v0.51.0"
3636

3737
# Status values:
3838
# implemented — shipped behavior matches the proposal's contract
@@ -202,6 +202,21 @@ status = "not-yet"
202202
[proposals."0020"]
203203
status = "not-yet"
204204

205+
# Spec (proposal 0021). Suspension capability — async-pause +
206+
# resume primitive (``suspend()`` + ``resume()``) layering on the
207+
# graph engine. Python has not yet shipped suspension; v0.13.0
208+
# leaves the capability not-yet-implemented.
209+
[proposals."0021"]
210+
status = "not-yet"
211+
212+
# Spec v0.49.0 (proposal 0022). Harness capability — abstract
213+
# contract for wrapping the engine in deployment runtimes
214+
# (HTTP / event-bus / queue / CLI / streaming). Python has not
215+
# yet shipped a harness binding; v0.13.0 leaves the capability
216+
# not-yet-implemented. Composes with 0056 (chat sub-spec).
217+
[proposals."0022"]
218+
status = "not-yet"
219+
205220
[proposals."0042"]
206221
status = "implemented"
207222
since = "0.11.0"
@@ -462,3 +477,35 @@ since = "0.12.0"
462477
[proposals."0054"]
463478
status = "implemented"
464479
since = "0.12.0"
480+
481+
# Spec v0.47.0 (proposal 0055). Conformance-adapter capability —
482+
# descriptive ratification of the existing fixture / directive
483+
# system. No code change; python's adapter is already structured
484+
# per the spec text by virtue of having grown alongside the
485+
# fixtures since proposal 0001. Matches the Textual impl-tracking
486+
# precedent (0019 / 0026 / 0030 / 0051 / 0053).
487+
[proposals."0055"]
488+
status = "textual-only"
489+
since = "0.13.0"
490+
491+
# Spec v0.48.0 (proposal 0056). Harness-chat capability — new
492+
# harness sub-spec ratifying the chat-loop deployment shape
493+
# (ChatMessage, conversation-history convention, send() callable,
494+
# send_streaming() forward-looking surface, error-bucket → user-
495+
# facing-reply mapping). Python does not yet ship a chat-harness
496+
# binding; v0.13.0 leaves the capability not-yet-implemented.
497+
[proposals."0056"]
498+
status = "not-yet"
499+
500+
# Spec v0.51.0 (proposal 0057). LlmCompletionEvent field-set
501+
# extension — eight additive request-side fields on the typed
502+
# event variant + ``request_id`` → ``response_id`` rename + new
503+
# ``response_model`` field. Python lands the field-set extension
504+
# + rename + provider population in v0.13.0 PR 3a; OTel + Langfuse
505+
# observers continue driving their §5.5 surface off the sentinel
506+
# NodeEvent pair through this PR (observer migration to type
507+
# discrimination is queued for follow-up PRs 3b / 3c against the
508+
# same v0.13.0 release).
509+
[proposals."0057"]
510+
status = "implemented"
511+
since = "0.13.0"

openarmature-spec

Submodule openarmature-spec updated 119 files

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ Specification = "https://github.com/LunarCommand/openarmature-spec"
6363
openarmature = "openarmature.cli:main"
6464

6565
[tool.openarmature]
66-
spec_version = "0.46.0"
66+
spec_version = "0.51.0"
6767

6868
[dependency-groups]
6969
dev = [

src/openarmature/AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# OpenArmature — Agent documentation
22

3-
*This is the agent guide bundled with the openarmature Python package, version 0.12.0 (spec v0.46.0). For the full docs site see [openarmature.ai](https://openarmature.ai). For the canonical spec text see [openarmature.org/capabilities](https://openarmature.org/capabilities/). For project-specific conventions for the code you're editing, see the host project's `AGENTS.md` or `CLAUDE.md`.*
3+
*This is the agent guide bundled with the openarmature Python package, version 0.12.0 (spec v0.51.0). For the full docs site see [openarmature.ai](https://openarmature.ai). For the canonical spec text see [openarmature.org/capabilities](https://openarmature.org/capabilities/). For project-specific conventions for the code you're editing, see the host project's `AGENTS.md` or `CLAUDE.md`.*
44

55
## TL;DR
66

@@ -10,7 +10,7 @@ OpenArmature is a workflow framework for LLM pipelines and tool-calling agents:
1010

1111
## Capability contracts
1212

13-
_Sourced from openarmature-spec v0.46.0. Each entry below reproduces §1 (Purpose) and §2 (Concepts) of the capability's `spec.md`. For the full spec text (execution model, error semantics, determinism, observer hooks, etc.) see the linked docs site._
13+
_Sourced from openarmature-spec v0.51.0. Each entry below reproduces §1 (Purpose) and §2 (Concepts) of the capability's `spec.md`. For the full spec text (execution model, error semantics, determinism, observer hooks, etc.) see the linked docs site._
1414

1515
### Capability: `graph-engine`
1616

src/openarmature/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"""
2626

2727
__version__ = "0.12.0"
28-
__spec_version__ = "0.46.0"
28+
__spec_version__ = "0.51.0"
2929
# Proposal 0052 (spec observability §5.1 / §8.4.1): canonical
3030
# package-registry name for this implementation. Surfaces on every
3131
# OTel invocation span as ``openarmature.implementation.name`` and on

src/openarmature/graph/events.py

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,22 @@ class InvocationCompletedEvent:
450450
#
451451
# Field naming matches the spec-canonical names verbatim per the spec
452452
# Q5 ack — Python snake_case happens to match the spec table 1:1.
453+
#
454+
# Spec proposal 0057 (v0.51.0) extension: adds 8 additive request-side
455+
# fields (input_messages, output_content, request_params,
456+
# request_extras, active_prompt, active_prompt_group, call_id,
457+
# response_model) and renames request_id → response_id to match the
458+
# response-side data the field carries. Inline image bytes in
459+
# input_messages MUST be redacted per observability §5.5.5 before
460+
# population — the provider reuses _serialize_messages_for_payload
461+
# which already enforces the redaction. The three payload-bearing
462+
# fields (input_messages, output_content, request_extras) are
463+
# populated unconditionally on the typed event per §5.5.7; observer-
464+
# side privacy gates (OTel disable_llm_payload, Langfuse equivalents)
465+
# apply at rendering, symmetric with the §5.5.1 span attribute path.
466+
# Custom queryable observers (per observability §9) own their own
467+
# redaction posture — gating belongs at rendering with the consumer's
468+
# awareness.
453469
@dataclass(frozen=True)
454470
class LlmCompletionEvent:
455471
"""A typed LLM provider call event delivered to observers.
@@ -473,17 +489,55 @@ class LlmCompletionEvent:
473489
- ``branch_name``: parallel-branches branch name when the
474490
calling node ran inside a branch; ``None`` otherwise.
475491
- ``provider``: provider identifier; matches ``gen_ai.system``.
476-
- ``model``: the model identifier the call targeted.
477-
- ``request_id``: provider-returned response id; ``None`` when
492+
- ``model``: the model identifier the call targeted (the
493+
request-side bound model; distinct from ``response_model``).
494+
- ``response_id``: provider-returned response id; ``None`` when
495+
the provider didn't return one.
496+
- ``response_model``: provider-returned model identifier;
497+
distinct from ``model`` (the provider may return a more
498+
specific identifier than the one requested). ``None`` when
478499
the provider didn't return one.
479-
- ``usage``: token-accounting record per ``Response.usage``
480-
shape. Reuses the existing ``openarmature.llm.response.Usage``
481-
class. ``None`` when the call returned no usage at all.
500+
- ``usage``: token-accounting record reusing the existing
501+
``openarmature.llm.response.Usage`` class. ``None`` when the
502+
call returned no usage at all.
482503
- ``latency_ms``: wall-clock latency measured at the adapter
483504
boundary, in milliseconds. ``None`` when latency was not
484505
measured.
485506
- ``finish_reason``: the call's finish reason; ``None`` when
486507
the call did not complete normally.
508+
- ``input_messages``: the message list the call was made with,
509+
serialized to the plain-dict shape. Non-nullable; empty list
510+
when the call had no history. Inline image bytes are
511+
redacted before population (see the comment block above for
512+
the redaction contract).
513+
- ``output_content``: the assistant message's content string
514+
from the response. ``None`` on tool-call-only responses
515+
(the structured-response and tool-call paths are mutually
516+
exclusive at the response level).
517+
- ``request_params``: the GenAI request-parameter set the
518+
caller supplied. Absence-is-meaningful: only caller-supplied
519+
keys appear; empty mapping when none supplied. Keys are the
520+
cross-vendor parameter names without the ``gen_ai.request.``
521+
prefix (e.g. ``temperature``, ``max_tokens``).
522+
- ``request_extras``: the ``RuntimeConfig`` extras pass-
523+
through bag in native mapping form (not JSON-encoded).
524+
Empty mapping when no extras supplied.
525+
- ``active_prompt``: 5-field identity snapshot of the active
526+
``PromptResult`` at LLM-call time (``name`` / ``version`` /
527+
``label`` / ``template_hash`` / ``rendered_hash``).
528+
``None`` when the call ran outside any prompt-context
529+
binding. Typed as ``Any`` because the prompts package
530+
imports State indirectly; observer-side narrowing reads
531+
the attribute names directly.
532+
- ``active_prompt_group``: ``{group_name}`` snapshot when the
533+
call ran inside a ``PromptGroup`` context; ``None``
534+
otherwise. Same ``Any`` typing rationale as
535+
``active_prompt``.
536+
- ``call_id``: per-call disambiguator minted by the
537+
implementation. Always present, freshly minted per
538+
``provider.complete()`` call, stable for the call's
539+
lifetime, unique within the run. Distinct from
540+
``response_id``.
487541
- ``caller_invocation_metadata``: optional snapshot of caller-
488542
supplied invocation metadata at LLM-call time. Populated
489543
only when the provider's opt-in flag is set (per-language
@@ -499,13 +553,26 @@ class LlmCompletionEvent:
499553
branch_name: str | None
500554
provider: str
501555
model: str
502-
request_id: str | None
556+
response_id: str | None
557+
response_model: str | None
503558
# Usage is a string-typed forward reference per the TYPE_CHECKING
504559
# import above — keeps the runtime import direction graph → llm
505560
# off the module-load path while preserving pyright resolution.
506561
usage: "Usage | None"
507562
latency_ms: float | None
508563
finish_reason: str | None
564+
# Proposal 0057 (spec v0.51.0) additive request-side fields.
565+
# Non-nullable for input_messages / request_params /
566+
# request_extras — absence is represented as empty list / empty
567+
# mapping, not None. output_content stays nullable for tool-
568+
# call-only assistant messages.
569+
input_messages: list[dict[str, Any]]
570+
output_content: str | None
571+
request_params: Mapping[str, Any]
572+
request_extras: Mapping[str, Any]
573+
active_prompt: Any
574+
active_prompt_group: Any
575+
call_id: str
509576
caller_invocation_metadata: Mapping[str, AttributeValue] | None = None
510577

511578

src/openarmature/llm/providers/openai.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -525,20 +525,46 @@ async def complete(
525525
# observers filtering on the sentinel namespace see the
526526
# NodeEvent pair above. Failure path doesn't reach here.
527527
dispatch(
528-
self._build_llm_completion_event(response, latency_ms),
528+
self._build_llm_completion_event(
529+
response,
530+
latency_ms,
531+
call_id=call_id,
532+
input_messages=serialized_messages,
533+
request_params=request_params,
534+
request_extras=request_extras,
535+
active_prompt=active_prompt,
536+
active_prompt_group=active_prompt_group,
537+
),
529538
)
530539
return response
531540

532-
def _build_llm_completion_event(self, response: Response, latency_ms: float) -> LlmCompletionEvent:
541+
def _build_llm_completion_event(
542+
self,
543+
response: Response,
544+
latency_ms: float,
545+
*,
546+
call_id: str,
547+
input_messages: list[dict[str, Any]],
548+
request_params: dict[str, Any],
549+
request_extras: dict[str, Any],
550+
active_prompt: Any,
551+
active_prompt_group: Any,
552+
) -> LlmCompletionEvent:
533553
"""Construct the typed LlmCompletionEvent for the success path.
534554
535555
Sources identity / scoping fields from the calling-node
536-
ContextVars and outcome fields from the response. The calling-
537-
node namespace is the FULL namespace tuple (not the legacy
538-
sentinel pseudo-namespace); node_name is the last element of
539-
the namespace (the user-defined node that issued the call).
540-
Outside any node body (namespace empty), node_name is the
541-
empty string.
556+
ContextVars and outcome fields from the response. Request-side
557+
fields (per proposal 0057) are passed through from the
558+
provider's complete() local state — serialized message list,
559+
the gen_ai.request.* parameter mapping, the RuntimeConfig
560+
extras, the prompt-context snapshots taken at dispatch time,
561+
and the call-id minted at the call's start.
562+
563+
The calling-node namespace is the FULL namespace tuple (not
564+
the legacy sentinel pseudo-namespace); node_name is the last
565+
element of the namespace (the user-defined node that issued
566+
the call). Outside any node body (namespace empty), node_name
567+
is the empty string.
542568
"""
543569

544570
namespace = current_namespace_prefix()
@@ -560,6 +586,14 @@ def _build_llm_completion_event(self, response: Response, latency_ms: float) ->
560586
# frozen view; if a node body mutates metadata after the
561587
# snapshot, the event still carries the at-emission view.
562588
caller_metadata = dict(current_invocation_metadata())
589+
# ``output_content`` is None on tool-call-only assistant
590+
# messages per llm-provider §6 mutual-exclusion: the
591+
# tool-call path and structured-content path are mutually
592+
# exclusive at the response level, and provider.complete()
593+
# leaves the AssistantMessage.content as the empty string on
594+
# the tool-call path (which we project to None per the
595+
# typed-event contract).
596+
output_content = response.message.content or None
563597
return LlmCompletionEvent(
564598
invocation_id=invocation_id,
565599
correlation_id=current_correlation_id(),
@@ -570,10 +604,18 @@ def _build_llm_completion_event(self, response: Response, latency_ms: float) ->
570604
branch_name=current_branch_name(),
571605
provider=self._genai_system,
572606
model=self.model,
573-
request_id=response.response_id,
607+
response_id=response.response_id,
608+
response_model=response.response_model,
574609
usage=response.usage,
575610
latency_ms=latency_ms,
576611
finish_reason=response.finish_reason,
612+
input_messages=input_messages,
613+
output_content=output_content,
614+
request_params=request_params,
615+
request_extras=request_extras,
616+
active_prompt=active_prompt,
617+
active_prompt_group=active_prompt_group,
618+
call_id=call_id,
577619
caller_invocation_metadata=caller_metadata,
578620
)
579621

tests/conformance/test_fixture_parsing.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,37 @@ def _id(case: tuple[str, Path]) -> str:
334334
"observability/056-llm-completion-event-strict-serial-ordering": (
335335
"Proposal 0049 typed LLM completion event; queued for v0.13.0"
336336
),
337+
# Proposal 0057 (LlmCompletionEvent field-set extension, v0.51.0)
338+
# — fixtures 060-068 share the same ``typed_observers`` directive
339+
# shape as 050-056 and inherit the same parser-deferral status
340+
# pending the harness model's typed-event-collector schema work.
341+
"observability/060-llm-completion-event-input-messages-populated": (
342+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
343+
),
344+
"observability/061-llm-completion-event-output-content-populated": (
345+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
346+
),
347+
"observability/062-llm-completion-event-request-params-populated": (
348+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
349+
),
350+
"observability/063-llm-completion-event-request-extras-populated": (
351+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
352+
),
353+
"observability/064-llm-completion-event-active-prompt-populated": (
354+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
355+
),
356+
"observability/065-llm-completion-event-active-prompt-null": (
357+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
358+
),
359+
"observability/066-llm-completion-event-active-prompt-group-populated": (
360+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
361+
),
362+
"observability/067-llm-completion-event-call-id-always-present-and-distinct": (
363+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
364+
),
365+
"observability/068-llm-completion-event-response-model-distinct-from-request": (
366+
"Proposal 0057 typed event request-side fields; queued for v0.13.0"
367+
),
337368
# Proposal 0050 (failure-isolation middleware + call-level retry,
338369
# v0.42.0) — llm-provider fixtures 056-058 (call-level retry) and
339370
# pipeline-utilities fixtures 058-063 (failure-isolation

tests/conformance/test_typed_event_harness.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,18 @@ def _make_typed_event(**overrides: Any) -> LlmCompletionEvent:
5151
"branch_name": None,
5252
"provider": "openai",
5353
"model": "gpt-test",
54-
"request_id": "req-1",
54+
"response_id": "req-1",
55+
"response_model": None,
5556
"usage": Usage(prompt_tokens=14, completion_tokens=4, total_tokens=18),
5657
"latency_ms": 42.0,
5758
"finish_reason": "stop",
59+
"input_messages": [],
60+
"output_content": None,
61+
"request_params": {},
62+
"request_extras": {},
63+
"active_prompt": None,
64+
"active_prompt_group": None,
65+
"call_id": "cc-1",
5866
"caller_invocation_metadata": None,
5967
}
6068
base.update(overrides)

tests/test_smoke.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
def test_package_versions() -> None:
1111
assert openarmature.__version__ == "0.12.0"
12-
assert openarmature.__spec_version__ == "0.46.0"
12+
assert openarmature.__spec_version__ == "0.51.0"
1313

1414

1515
def test_spec_version_matches_pyproject() -> None:

0 commit comments

Comments
 (0)