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
12 changes: 9 additions & 3 deletions conformance.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

[manifest]
implementation = "openarmature-python"
spec_pin = "v0.24.0"
spec_pin = "v0.26.0"

# Status values:
# implemented — shipped behavior matches the proposal's contract
Expand Down Expand Up @@ -150,8 +150,8 @@ status = "textual-only"
since = "0.9.0"
note = "Drain snapshot semantic and timeout-input validation already implemented as part of the proposal 0010 impl PR (v0.9.0); no additional module-level work needed."

# Spec v0.23.0 + v0.24.0 batch (proposals 0031, 0032). Both proposals
# have impl work landing across the v0.10.0 release cycle; status
# Spec v0.23.0-v0.26.0 batch (proposals 0031, 0032, 0033, 0034). All
# four have impl work landing across the v0.10.0 release cycle; status
# stays `not-yet` until the release PR flips them to `implemented`
# with `since = "0.10.0"`. The pinned spec submodule advances ahead
# of the impl status because newer fixtures need to be visible to
Expand All @@ -161,3 +161,9 @@ status = "not-yet"

[proposals."0032"]
status = "not-yet"

[proposals."0033"]
status = "not-yet"

[proposals."0034"]
status = "not-yet"
86 changes: 85 additions & 1 deletion docs/concepts/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,79 @@ a working-but-wrong prompt, often invisibly. If you need
lenient behavior, wrap your variables in your own defaulting
layer before passing them to `render()`.

The Python implementation uses Jinja2's `StrictUndefined`.
The Python implementation uses Jinja2's `StrictUndefined`. To opt
out, pass a different `Undefined` subclass at `PromptManager`
construction:

```python
import jinja2

manager = PromptManager(backend, jinja_undefined=jinja2.Undefined)
```

`jinja2.Undefined` renders a missing variable as the empty string;
`jinja2.ChainableUndefined` is the other common opt-out for
templates that walk nested attributes. Reach for these only when the
strict default is actively wrong for your workflow.

## Per-prompt sampling parameters

A `Prompt` carries an optional `sampling` field — a `SamplingConfig`
sub-record mirroring `RuntimeConfig`'s seven declared fields
(`temperature`, `max_tokens`, `top_p`, `seed`, `frequency_penalty`,
`presence_penalty`, `stop_sequences`) plus the extras pass-through
bag. Backends that source per-prompt config (Langfuse's
`prompt.config`, a filesystem sidecar) populate it; backends that
don't leave it `None`.

```python
prompt = await manager.fetch("classify", "production")
if prompt.sampling is not None:
response = await provider.complete(messages, config=prompt.sampling)
else:
response = await provider.complete(messages)
```

`SamplingConfig` is a subclass of `RuntimeConfig`, so it splats
directly into `provider.complete()` without translation.
`PromptResult.sampling` carries the value verbatim from the source
`Prompt`; rendering doesn't touch it.

The `FilesystemPromptBackend` reads sidecar config when constructed
with `sampling_source="per-prompt-sidecar"` (reading
`<root>/<label>/<name>.config.json` next to each template) or
`sampling_source="unified"` (reading `<root>/prompt_configs.json`
once at construction, keyed by prompt name).

## Deployment-time label routing with `LabelResolver`

`PromptManager.fetch(name)` without an explicit `label` consults a
configured `LabelResolver` and falls back to `"production"`. This
lets one prompt be A/B-tested or canaried without code changes —
edit the resolver's data, not the call sites.

```python
from openarmature.prompts import MappingLabelResolver, PromptManager

resolver = MappingLabelResolver({
"default": "production",
"experimental_classifier": "staging",
"extract_claims": "variant-a",
})
manager = PromptManager(backend, label_resolver=resolver)

# Resolver returns "staging" — staging template fetched.
classify = await manager.fetch("experimental_classifier")
# Resolver returns "production" (the default) — production fetched.
greet = await manager.fetch("greet")
# Explicit label bypasses the resolver entirely.
audit = await manager.fetch("greet", "audit")
```

`LabelResolver` is a Protocol with one method, `resolve(name) -> str`.
The reference implementation is `MappingLabelResolver`, but any
class with the right shape works (a JSON-file-backed resolver, a
remote-config-service-backed resolver).

## Composite backends and fallback

Expand Down Expand Up @@ -212,6 +284,18 @@ Nesting is innermost-wins. If you activate a result inside
another active result, the inner one wins for the duration
of the inner block.

### Backend-keyed observability entity references

A `Prompt` also carries an optional `observability_entities`
mapping for backend-keyed references to first-class entities
the prompt has been registered as in observability backends. The
spec-normative key is `langfuse_prompt`, holding the Langfuse SDK
`Prompt` reference. The Langfuse observer (when it ships) reads
this field to establish the native Generation → Prompt link
rather than reaching into the implementation-defined `metadata`
mapping. Backends that don't surface such references leave the
field `None`.

## Determinism and content-addressed caching

`render` is deterministic: same `Prompt`, same `variables` →
Expand Down
2 changes: 1 addition & 1 deletion openarmature-spec
Submodule openarmature-spec updated 28 files
+39 −0 CHANGELOG.md
+4 −4 README.md
+2 −0 docs/proposals.md
+1 −0 docs/proposals/0033-prompt-management-surface-refinements.md
+1 −0 docs/proposals/0034-caller-supplied-invocation-metadata.md
+548 −0 proposals/0033-prompt-management-surface-refinements.md
+474 −0 proposals/0034-caller-supplied-invocation-metadata.md
+11 −0 spec/graph-engine/spec.md
+45 −0 spec/observability/conformance/026-otel-caller-supplied-metadata.md
+78 −0 spec/observability/conformance/026-otel-caller-supplied-metadata.yaml
+52 −0 spec/observability/conformance/027-langfuse-caller-supplied-metadata.md
+87 −0 spec/observability/conformance/027-langfuse-caller-supplied-metadata.yaml
+51 −0 spec/observability/conformance/028-caller-metadata-namespace-rejection.md
+59 −0 spec/observability/conformance/028-caller-metadata-namespace-rejection.yaml
+67 −0 spec/observability/conformance/029-caller-metadata-fan-out-per-instance.md
+144 −0 spec/observability/conformance/029-caller-metadata-fan-out-per-instance.yaml
+70 −0 spec/observability/conformance/030-caller-metadata-parallel-branches-per-branch.md
+147 −0 spec/observability/conformance/030-caller-metadata-parallel-branches-per-branch.yaml
+163 −8 spec/observability/spec.md
+37 −0 spec/prompt-management/conformance/013-prompt-sampling-from-backend.md
+69 −0 spec/prompt-management/conformance/013-prompt-sampling-from-backend.yaml
+37 −0 spec/prompt-management/conformance/014-prompt-sampling-absent.md
+32 −0 spec/prompt-management/conformance/014-prompt-sampling-absent.yaml
+61 −0 spec/prompt-management/conformance/015-label-resolver-fallback-chain.md
+124 −0 spec/prompt-management/conformance/015-label-resolver-fallback-chain.yaml
+56 −0 spec/prompt-management/conformance/016-prompt-observability-entities-propagation.md
+80 −0 spec/prompt-management/conformance/016-prompt-observability-entities-propagation.yaml
+171 −27 spec/prompt-management/spec.md
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Specification = "https://github.com/LunarCommand/openarmature-spec"
openarmature = "openarmature.cli:main"

[tool.openarmature]
spec_version = "0.24.0"
spec_version = "0.26.0"

[dependency-groups]
dev = [
Expand Down
4 changes: 2 additions & 2 deletions 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.9.0 (spec v0.24.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.9.0 (spec v0.26.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 All @@ -10,7 +10,7 @@ OpenArmature is a workflow framework for LLM pipelines and tool-calling agents

## Capability contracts

_Sourced from openarmature-spec v0.24.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._
_Sourced from openarmature-spec v0.26.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._

### Capability: `graph-engine`

Expand Down
2 changes: 1 addition & 1 deletion src/openarmature/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"""

__version__ = "0.9.0"
__spec_version__ = "0.24.0"
__spec_version__ = "0.26.0"
13 changes: 12 additions & 1 deletion src/openarmature/llm/providers/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@
current_namespace_prefix,
)
from openarmature.observability.llm_event import LlmEventPayload
from openarmature.prompts.context import current_prompt_group, current_prompt_result

# ``current_prompt_group`` / ``current_prompt_result`` are imported
# lazily inside :meth:`OpenAIProvider.complete` to avoid a module-load
# cycle: ``openarmature.prompts.prompt`` imports ``RuntimeConfig`` from
# this package (for the ``SamplingConfig`` subclass), so a top-level
# import here would re-enter prompts.prompt before its types finish
# defining.
from ..errors import (
LlmProviderError,
ProviderAuthentication,
Expand Down Expand Up @@ -310,6 +315,12 @@ async def complete(
# from inside the observer in the worker task returns ``None``
# even when a node body opened a ``with_active_prompt`` block.
# Snapshot here; the observer reads from the event payload.
# Lazy import: see module-level comment for the cycle reason.
from openarmature.prompts.context import (
current_prompt_group,
current_prompt_result,
)

active_prompt = current_prompt_result()
active_prompt_group = current_prompt_group()
# Payload data the §5.5.1 / §5.5.2 / §5.5.3 attributes are
Expand Down
7 changes: 6 additions & 1 deletion src/openarmature/prompts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,19 @@
)
from .group import PromptGroup
from .hashing import compute_rendered_hash, compute_template_hash
from .label_resolver import SPEC_FALLBACK_LABEL, LabelResolver, MappingLabelResolver
from .manager import PromptManager
from .prompt import Prompt, PromptResult
from .prompt import Prompt, PromptResult, SamplingConfig

__all__ = [
"PROMPT_NOT_FOUND",
"PROMPT_RENDER_ERROR",
"PROMPT_STORE_UNAVAILABLE",
"PROMPT_TRANSIENT_CATEGORIES",
"SPEC_FALLBACK_LABEL",
"FilesystemPromptBackend",
"LabelResolver",
"MappingLabelResolver",
"Prompt",
"PromptBackend",
"PromptError",
Expand All @@ -38,6 +42,7 @@
"PromptRenderError",
"PromptResult",
"PromptStoreUnavailable",
"SamplingConfig",
"compute_rendered_hash",
"compute_template_hash",
"current_prompt_group",
Expand Down
Loading