Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ All notable changes to `openarmature-python` are documented in this file.

The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The package follows [Semantic Versioning](https://semver.org/); pre-1.0 minor bumps may carry behavioral changes per [spec governance](https://github.com/LunarCommand/openarmature-spec/blob/main/GOVERNANCE.md).

## [Unreleased]
## [0.11.0] — 2026-06-01

Observability + prompt-management release. The pinned spec advances from v0.27.1 to v0.38.0, absorbing eight accepted proposals (0039-0046). Two headlines: (1) the Langfuse observer grows native `trace.input` / `trace.output` sourcing with caller hooks (0043) and the per-async-context augmentation boundary becomes lineage-aware for nested fan-out / parallel-branches topologies (0045); (2) prompt-management gains a Chat-prompt variant alongside the existing Text-prompt (0046) and `LangfusePromptBackend` lands for both Langfuse text and chat prompts. Caller-supplied `invocation_id` (0039), mid-invocation open-span metadata update (0040), three reserved-key surfaces (0041 + 0042), and the parallel-branches OTel dispatch span (0044) round out the cycle.

### Added

Expand All @@ -25,6 +27,9 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The
- `openarmature.parallel_branches.branch_count` + `openarmature.parallel_branches.error_policy` on the parallel-branches NODE span.
- `openarmature.node.branch_name` + `openarmature.parallel_branches.parent_node_name` on each per-branch dispatch span.
- `openarmature.node.branch_name` on every inner-node span beneath a per-branch dispatch span.
- **Caller-supplied `invocation_id`** (proposal 0039, observability §5.1, spec v0.32.0). `invoke(invocation_id=...)` now accepts a caller-supplied non-empty URL-safe string in place of the framework-minted UUIDv4. Mirrors the `correlation_id` shape: caller-supplied wins; framework mints a UUIDv4 only when absent. Resume mints a fresh `invocation_id` per attempt (the previous attempt's id remains on the saved record). The Langfuse mapping derives `trace.id` via the SDK's `create_trace_id(seed=invocation_id)` for non-UUID values (raw id preserved under `trace.metadata.invocation_id`); UUID values continue to map via dashes-stripped hex.
- **Mid-invocation open-span metadata update** (proposal 0040, observability §3.4, spec v0.31.0). `set_invocation_metadata(**entries)` called mid-invocation now updates currently-open spans in the augmenting async context in place, via the backend SDK's attribute / metadata update path (`Span.set_attribute` for OTel, observation `update(metadata=...)` for Langfuse). Tightens 0034's per-async-context delivery from SHOULD to MUST; preserves the ancestor / sibling boundary (spans in ancestor or sibling contexts MUST NOT be updated). Per spec §3.4 v0.31.0; proposal 0045 (v0.37.0) extends the boundary rule to be lineage-aware for nested dispatch.
- **`LangfusePromptBackend`** (text and chat variants). A `PromptBackend` impl backed by the Langfuse SDK's prompt-registry surface. Gated behind the existing `[langfuse]` extra so the base package stays SDK-free. Maps Langfuse `TextPromptClient` to `TextPrompt`; `ChatPromptClient` to `ChatPrompt` (added by proposal 0046 — see entry above). Fails closed (`PromptNotFound`) when a Langfuse chat entry has an unsupported shape rather than silently dropping. The fetched `Prompt` carries the SDK `Prompt` object under `observability_entities['langfuse_prompt']` so the existing Generation → Prompt link in the Langfuse observer fires automatically.

### Changed (breaking, pre-1.0)

Expand All @@ -33,18 +38,20 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The

### Changed

- **Reserved-key extension** (proposal 0042, observability §3.4). Three additional bare key names — `branch_name`, `detached`, `detached_from_invocation_id` — are reserved against caller-supplied `invocation_metadata` and `set_invocation_metadata` collision; the framework rejects them at the `invoke()` boundary and at the mid-invocation augmentation helper with `ValueError`. The reserved-name set grows from 21 to 24. These three are top-level Langfuse metadata keys the observer mapping already writes; without reservation a caller key matching one would silently shadow the OA-emitted field.
- **Reserved-key extension** (proposal 0042, observability §3.4, spec v0.34.0). Three additional bare key names — `branch_name`, `detached`, `detached_from_invocation_id` — are reserved against caller-supplied `invocation_metadata` and `set_invocation_metadata` collision; the framework rejects them at the `invoke()` boundary and at the mid-invocation augmentation helper with `ValueError`. The reserved-name set grows from 21 to 24. These three are top-level Langfuse metadata keys the observer mapping already writes; without reservation a caller key matching one would silently shadow the OA-emitted field. Maintenance extension of the 0041 mechanism.
- **Langfuse-emitted top-level metadata key collision reservation** (proposal 0041, observability §3.4, spec v0.31.0). The reserved caller-metadata key set extends to cover every OA-emitted top-level key the §8.4 Langfuse mapping writes alongside caller keys (20 names — `correlation_id`, `entry_node`, `spec_version`, `namespace`, `step`, `attempt_index`, `fan_out_index`, `subgraph_name`, etc.). Whole-key exact match; rejected at the `invoke()` boundary independently of which backend is attached. Prevents a caller key from silently overwriting an OA-emitted field in Langfuse's flat top-level metadata. Pre-existing `openarmature.*` / `gen_ai.*` prefix reservation unchanged.
- **`observation.metadata.detached: true` moves to the parent-side dispatching observation** (proposal 0042, observability §8.4.2). The Langfuse mapping previously emitted `detached: true` on the dispatch observation inside the detached child trace; the §8.4.2 row added by 0042 places it on the **parent-side** dispatching observation that fires the detached child (the link observation in the main trace for detached subgraphs; the parent fan-out node observation for detached fan-outs). The detached-side observation no longer carries the flag.
- **`LangfuseClient.update_trace` Protocol grows `input` / `output` keyword parameters** so observer-supplied values land on the Trace's headline fields.

### Fixed

- **`install_log_bridge` no-ops when an OTel logging handler is already attached** to the root logger by an SDK that auto-installs one (e.g., HyperDX). The previous attach-always behavior produced duplicate `LoggingHandler` instances and double-emitted log records when used alongside such an SDK. The installer now detects an existing `LoggingHandler` whose provider matches the current `LoggerProvider` and skips the re-attach.
- **`InvocationCompletedEvent.final_state` on the failure path now surfaces the partial state at failure point.** Spec §8.4.1 *Resume semantics* requires the failure-path `trace.output` hook to receive "the partial final state captured at the failure point"; the original PR #99 implementation defaulted to `starting_state`, so the hook saw pre-execution state when it should have seen post-execution-up-to-failure state. The engine now tracks the latest post-merge state via a `latest_state_box` on `_InvocationContext`, updated after every successful step and read on the failure path. Success-path behavior unchanged.
- **`latest_state_box` is per-context, not shared across subgraph descents.** Unlike the sibling `final_node_box` (which shares by reference because the spec wants the innermost failing node's name — the real culprit), `latest_state_box` must isolate per level so the outermost Langfuse trace receives outer-state-typed values. Without the isolation, a subgraph-internal step's inner-typed state would leak up to the outer trace.output hook, breaking the hook's typed contract. Each subgraph / fan-out instance / parallel-branches branch gets its own fresh box. Pinned by four regression tests covering flat, subgraph, fan-out, and parallel-branches failure paths.

### Notes

- **Pinned spec version bumped from v0.31.0 to v0.36.0.** Absorbs proposals 0042 (reserved-key extension), 0043 (Langfuse trace.input/output sourcing), 0044 (parallel-branches OTel dispatch span), and the textual additions in v0.32.0 (Gemini wire-format mapping, 0038, not yet implemented) and v0.33.0 (sessions capability, 0020, not yet implemented).
- **Pinned spec version bumped from v0.27.1 to v0.38.0 over the v0.11.0 cycle.** Eight proposals absorbed: 0039 (caller-supplied invocation_id, v0.32.0), 0040 (mid-invocation open-span metadata update, v0.31.0), 0041 (Langfuse top-level metadata key collision reservation, v0.31.0), 0042 (reserved-key extension to 24 names, v0.34.0), 0043 (Langfuse trace input/output sourcing, v0.35.0), 0044 (parallel-branches OTel dispatch span, v0.36.0), 0045 (nested-lineage augmentation containment scope, v0.37.0), and 0046 (multi-message chat-prompt rendering, v0.38.0). The pinned spec also carries the textual additions in v0.32.0 (Gemini wire-format mapping, 0038, not yet implemented) and v0.33.0 (sessions capability, 0020, not yet implemented).
- `LangfuseSDKAdapter` now applies `trace.input` / `trace.output` to the live Langfuse Trace. Input lands on the first real observation under the trace via `set_trace_io`; output uses a synthetic short-lived `openarmature.trace_io` observation as the carrier. The InMemoryLangfuseClient used by tests applies the fields directly.
- Conformance fixture `observability/conformance/037-langfuse-trace-input-output` activated for all five cases (default stub / `disable_state_payload=False` / hooks non-null / hooks null-fallthrough / resume re-fire). The langfuse harness grew per-case `checkpointer: in_memory` wiring, a compact `flaky:` test seam, and a two-phase resume-flow assertion path.
- The Langfuse v4 SDK marks `set_current_trace_io` / `Span.set_trace_io` deprecated ("removal in a future major version"). Empirical verification against Langfuse Cloud (v4.7.1, 2026-05-29) confirms it remains the **only** path that populates the Traces list view's headline `Input` / `Output` columns; `propagate_attributes(metadata=...)` does not substitute for it in the current UI. We will revisit when Langfuse publishes a concrete migration guide for v5.
Expand Down
4 changes: 2 additions & 2 deletions docs/concepts/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ A walk-through:
instance contains a parallel-branches node). The combination
`(namespace, branch_name, fan_out_index, attempt_index, phase)`
uniquely identifies each event source. On the OTel mapping
side, an `openarmature.branch_name` span attribute is added in
parallel to the existing `openarmature.node.fan_out_index`.
side, an `openarmature.node.branch_name` span attribute is added
in parallel to the existing `openarmature.node.fan_out_index`.

## Routing errors and the completed event

Expand Down
29 changes: 29 additions & 0 deletions docs/concepts/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,35 @@ manager = PromptManager(backend, jinja_undefined=jinja2.Undefined)
templates that walk nested attributes. Reach for these only when the
strict default is actively wrong for your workflow.

## Two variants: text and chat

`Prompt` is a discriminated union over `TextPrompt` and `ChatPrompt`:

- A `TextPrompt` carries a single `template: str` and renders to
exactly one `UserMessage`. This is the simpler variant and the
default for the filesystem backend; reach for it when the prompt
is a single user instruction and you don't need role tagging.
- A `ChatPrompt` carries `chat_template: list[ChatSegment]`. Each
segment is either a `ContentSegment` (a role-tagged content
block — `system`, `user`, or `assistant`, carrying a text
template OR a list of content-block templates for multimodal
user messages) or a `PlaceholderSegment` (a slot the caller fills
at render time with a `list[Message]`, useful for chat history
injection).

`PromptManager.render(prompt, variables, placeholders=...)`
dispatches on the variant. For `TextPrompt` the `placeholders`
kwarg is ignored. For `ChatPrompt` each content segment renders
with the strict-undefined rule applied independently; placeholder
segments inject their caller-supplied message lists in order.

Backends can return either variant — the `LangfusePromptBackend`
maps Langfuse text prompts to `TextPrompt` and Langfuse chat
prompts to `ChatPrompt` with one `ContentSegment` per Langfuse
chat message. Discriminate at the call site with
`isinstance(prompt, ChatPrompt)` when you need variant-specific
behavior; most callers just pass the prompt back into `render()`.

## Per-prompt sampling parameters

A `Prompt` carries an optional `sampling` field — a `SamplingConfig`
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "openarmature"
version = "0.10.0"
version = "0.11.0rc1"
description = "Workflow framework for LLM pipelines and tool-calling agents."
readme = "README.md"
requires-python = ">=3.12"
Expand Down
2 changes: 1 addition & 1 deletion src/openarmature/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# OpenArmature — Agent documentation

*This is the agent guide bundled with the openarmature Python package, version 0.10.0 (spec v0.38.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`.*
*This is the agent guide bundled with the openarmature Python package, version 0.11.0rc1 (spec v0.38.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`.*

## TL;DR

Expand Down
2 changes: 1 addition & 1 deletion src/openarmature/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
sessions opening the project find the bundled docs automatically.
"""

__version__ = "0.10.0"
__version__ = "0.11.0rc1"
__spec_version__ = "0.38.0"
2 changes: 1 addition & 1 deletion tests/test_smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def test_package_versions() -> None:
assert openarmature.__version__ == "0.10.0"
assert openarmature.__version__ == "0.11.0rc1"
assert openarmature.__spec_version__ == "0.38.0"


Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.