All notable changes to openarmature-python are documented in this file.
The format follows Keep a Changelog. The package follows Semantic Versioning; pre-1.0 minor bumps may carry behavioral changes per spec governance.
openarmature.patternsprogrammatic API. Two-function surface (list() -> list[str],get(name: str) -> str) exposing the same patterns content shipped in the bundledAGENTS.md. Each pattern is returned as a standalone markdown document: no heading demotion (patterns keep their original# Title), and relative../concepts/...md/../examples/...md/ intra-pattern links are rewritten to absoluteopenarmature.aiURLs at build time so cross-references resolve outside the source tree. Useful for agents in sandboxed environments that canimport openarmaturebut can't freely read arbitrary package paths. Content lives atsrc/openarmature/_patterns/<slug>.md, generated alongside the bundledAGENTS.mdand drift-checked bytests/test_agents_md_drift.py. Unknown names raiseKeyErrorwith a message listing the known names.openarmatureCLI registered as a[project.scripts]entry point with two subcommands:openarmature initappends a discovery pointer block (thepython -c "..."one-liner +openarmature docsrecipe) into the current project'sAGENTS.mdandCLAUDE.mdso agent sessions opening the project find the bundled OpenArmature docs. Creates files when absent, appends when they exist, and skips re-runs via a<!-- openarmature-init -->comment marker. Flags:--force(re-append despite the marker),--dry-run(print what would be written),--cwd PATH(operate against a path other than the current directory).openarmature docsprints the absolute path to the bundledAGENTS.md. Equivalent to the README discovery one-liner but ergonomic to type and remember.- The same surface is reachable as
python -m openarmature ...viasrc/openarmature/__main__.py, so environments where the[project.scripts]entry doesn't land cleanly (somepip install --targetlayouts, path-shadowed venvs) still work as long as the package is importable.
- Bundled agent documentation at
openarmature/AGENTS.md. The wheel now ships a generatedAGENTS.mdfile at the installed package root, agent-discoverable viapython -c "import openarmature; print(openarmature.__path__[0] + '/AGENTS.md')". Sections include a TL;DR, capability summaries pulled from the pinned spec submodule's §1 (Purpose) + §2 (Concepts), the patterns docs, hand-written non-obvious-shapes recipes, and a one-line example index. Generator lives atscripts/build_agents_md.py; the committed file is CI-drift-checked bytests/test_agents_md_drift.py. The submodule pin discipline (build refuses unless the submodule HEAD is AT av*tag viagit tag --points-at HEAD) prevents draft (untagged) spec text — or text from a commit between two release tags — from leaking into a release bundle. Adopting projects can point their ownAGENTS.md/CLAUDE.mdat this path so agent sessions in their codebase find it automatically (or useopenarmature initto do the wiring automatically). FanOutInstanceProgress.result_is_errorfield (proposal 0027, accepted in spec v0.21.0). Explicit boolean discriminator on each per-instance entry inCheckpointRecord.fan_out_progress—Trueforcollect-mode error contributions (roll forward intoerrors_field),Falsefor success contributions (roll forward intotarget_field). The engine reads the explicit field on resume rather than inferring routing fromresult's shape; the previous structural heuristic (_looks_like_error_record) is removed. Backward-compat path on load: pre-0027 records that omit the key default toFalse.- Strict
CheckpointRecordInvalidon fan-out count drift (proposal 0029, accepted in spec v0.22.0). When the resumed run's resolved instance count differs from the savedfan_out_progressentry'sinstance_count, the engine raisesCheckpointRecordInvalidbefore any fan-out instance work runs on the resumed path. Replaces the pre-0029 pad/truncate behavior which silently droppedcompletedcontributions on shrink (breaking §10.11.1's exactly-once guarantee) and dispatched unsaved work on grow. tool_choiceparameter onProvider.complete()(proposal 0025, accepted in spec v0.20.0). Optional discriminated-union value constraining the model's tool-calling behavior — one of"auto","required","none", or aForceTool(name=...)record. Validation runs pre-send:"required"andForceToolboth demand non-emptytools, andForceTool.namemust appear in the supplied list; violations raiseProviderInvalidRequest(§7's existing category — no new error category). Whentool_choiceisNone(the default) the wire field is omitted and the provider's own default applies, preserving pre-0025 behavior exactly. TheOpenAIProvidermaps the spec shape onto OpenAI's wire shape per §8.1.1 (theForceTool.type="tool"renames to wiretype="function").ForceToolandToolChoicepublic types atopenarmature.llm.ForceTool/openarmature.llm.ToolChoice.ForceToolis a frozen Pydantic model withtype: Literal["tool"] = "tool"andname: str;ToolChoice = Literal["auto", "required", "none"] | ForceToolis the type alias used inProvider.complete()'s signature.validate_tool_choicepublic validator atopenarmature.llm.validate_tool_choice. Standalone validator covering the three §5 pre-send rules; useful for third-partyProviderimplementations that want to reuse the canonical validation logic.- Bounded drain timeout on
CompiledGraph.drain()(proposal 0010, accepted in spec v0.19.0).drain()accepts an optionaltimeout: float | None = Noneparameter (non-negative seconds). When supplied, drain returns no later than the deadline; any observer events still queued or in-flight are reported as undelivered. Workers are cancelled cleanly so the compiled graph remains usable for subsequent invocations — partial delivery state from one drain does NOT leak into the next. Solves the "slow / hung / misbehaving observer blocks process exit" footgun for short-lived processes (CLIs, scripts, serverless functions). Observers SHOULD be cancellation-safe (idempotent writes,try/finallycleanup); the spec doesn't mandate it but the docs recommend it. DrainSummaryfrozen dataclass atopenarmature.graph.DrainSummary. Returned from everydrain()call (with or withouttimeout). Fields:undelivered_count: int,timeout_reached: bool. The shape is consistent across timed and untimed drains — callers receive the same dataclass whether the timeout was supplied or not. Per the v0.19.0 contract the two declared fields are the spec-mandated minimum; richer diagnostic detail (per-observer counts, sampled event metadata) is reserved for follow-on PRs.- Per-instance fan-out resume contract (proposal 0009, accepted in spec v0.18.0). The engine now writes a checkpoint record at every
completedevent inside a fan-out instance (in addition to the existing outermost-graph + subgraph-internal + fan-out node completion saves). On resume the engine consults the saved record'sfan_out_progressfield and treats each instance ascompleted(skip, contribution rolls forward),in_flight(re-run from subgraph entry), ornot_started(dispatch normally). Theappendreducer's no-double-merge guarantee holds across resume becausecompletedis a one-shot accumulator state. FanOutProgressandFanOutInstanceProgresspublic dataclasses onopenarmature.checkpoint. TheCheckpointRecord.fan_out_progressfield is nowtuple[FanOutProgress, ...](default empty tuple), with per-instance state, result, andcompleted_inner_positionsobservability. Was aNoneplaceholder under proposal 0008.FanOutInternalSaveBatchingconfig onInMemoryCheckpointer. Backends MAY opt into batching scoped to fan-out instance internal saves to bound the write volume of high-instance-count fan-outs. Outermost-graph, subgraph-internal, and the fan-out node's own completion save remain synchronous regardless. Default off. Buffered-but-unflushed saves are lost on crash by design; on resume, instances whosecompletedstate was only buffered revert and re-run. Surfaces a new optionalsave_fan_out_internal/save_fan_out_in_flight_failureCheckpointer Protocol seam; backends that don't implement either fall back to the standardsave.- Patterns docs section at
docs/patterns/, sibling to Concepts. Seeded with four recipes drawn from downstream usage and proposal 0008's alternatives section: parameterized entry point, tool-dispatch-as-node, session-as-checkpoint-resume, and bypass-if-output-exists. Patterns are user-level how-to recipes composing existing primitives, not framework contracts; new patterns can be added without spec coordination. Each page follows a problem / approach / snippet / when this is the right pattern / when it isn't / cross-references structure.
CheckpointRecord.schema_versionsourcing clarified per proposal 0028 (spec v0.21.1). Every save site within an invocation now readsschema_versionfrom the declared graph state class — the class passed toGraphBuilder(...)— threaded ascontext.state_cls. Previously the outer dispatch save read from the declared class while fan-out instance internal saves read fromtype(state)at save time; the inconsistency only surfaced when a user passed a State subclass that shadowedschema_version, but the divergence made§10.12migration lookups unreliable across save sites. Now uniform across outer / subgraph-internal / fan-out instance internal saves.Provider.complete()signature extended with an optionaltool_choice: ToolChoice | None = Noneparameter (per proposal 0025 v0.20.0). Backward-compatible: callers that omit the new argument see no wire-shape change. Third-partyProviderimplementations MUST add the parameter to remain Protocol-conformant under strict type checking (and to accept calls that passtool_choicewithout raisingTypeError); they MAY ignore it in their wire-body emission, which is how "provider doesn't honor tool_choice" looks at the impl level. TheOpenAIProviderwire mapping is implemented per §8.1.1.CompiledGraph.drain()return type changed fromNonetoDrainSummary(pre-1.0; per proposal 0010 v0.19.0 contract). Callers that ignored the return are unaffected —await graph.drain()discards the returned dataclass exactly as before. Callers that explicitly typed the return asNonewill need to update their annotation.- Fan-out resume behavior flipped from atomic restart (0008's v1 contract) to per-instance resume. A crash mid-fan-out used to re-run the entire fan-out on resume; now only the instances that did not complete-and-record their contribution re-run. The economics matter for large fan-outs of expensive work (LLM calls, long extractions): an 80% complete fan-out crash now restores 80% of its results rather than discarding them.
SQLiteCheckpointerschema picks up a newfan_out_progress_blobcolumn (added viaALTER TABLEfor backward compatibility with pre-0009 databases). Pre-0009 rows back-fill as NULL on load and round-trip as the empty-tuple default. Bothpickleandjsonserialization modes round-trip the new field.
- Pinned spec version bumped from v0.17.0 to v0.22.1 over the v0.9.0 cycle. Ten spec versions absorbed: v0.17.1 (proposal 0019, multi-provider wire-format extension — purely textual reframe of llm-provider §8 as a catalog of wire-format mappings; OpenAI-compatible body nested under §8.1), v0.18.0 (proposal 0009, per-instance fan-out resume — pipeline-utilities §10.3 / §10.7 revised, §10.11 added; the
appendreducer no-double-merge invariant is the load-bearing correctness story), v0.18.1 (fixture-only patch correcting an off-by-one literal in fixture 052's expectedresults), v0.19.0 (proposal 0010, bounded drain timeout — graph-engine §6 amended with thetimeoutparameter andDrainSummaryreturn contract), v0.20.0 (proposal 0025, llm-providertool_choice— §5 / §7 / §8.1.1 amended), v0.20.1 (proposal 0026, llm-provider §8.X wire-format mapping subsection template — purely textual §8 framing paragraph; the existing OpenAI §8.1 mapping is the template's reference shape so no python module-level work was needed), v0.21.0 (proposal 0027, explicitresult_is_errordiscriminator onfan_out_progressper-instance entries — see Added above), v0.21.1 (proposal 0028, canonical source forschema_version— declared graph state class wins over runtime subclass shadowing; see Changed above), v0.22.0 (proposal 0029, strictCheckpointRecordInvalidon fan-out count drift — see Added above), and v0.22.1 (proposal 0030, drain snapshot semantic + timeout-input validation — purely textual; python already implemented both behaviors per the 0010 impl PR, so no module-level work needed). All existing conformance fixtures continue to pass.
LLM-provider span payload and GenAI semconv release. Pinned spec jumps from v0.16.1 to v0.17.0 (proposal 0024 / observability §5.5 expansion). The trigger was a friction report from a downstream agent integrating OA with Langfuse over OTLP: LLM spans rendered "naked" (model + tokens only), prompt linkage silently dropped at the dispatch-worker task boundary, and every backend needed a per-service attribute-mapping shim. This release clears all eight items in that report.
openarmature.llm.input.messages/openarmature.llm.output.content/openarmature.llm.request.extrasspan attributes (spec §5.5.1). When the OTel observer is constructed withdisable_llm_payload=False, LLM spans carry the messages sent, the assistant response content, and theRuntimeConfigextras bag — JSON-encoded with sorted keys, no insignificant whitespace, UTF-8. Default-off (the flag isdisable_llm_payload: bool = True) because the payload may contain PII the user hasn't audited; opt in deliberately. Subject to the §5.5.5 truncation contract.- GenAI semantic-conventions attributes (spec §5.5.2 + §5.5.3). LLM spans now carry
gen_ai.system,gen_ai.request.model,gen_ai.response.model,gen_ai.usage.input_tokens,gen_ai.usage.output_tokens,gen_ai.response.finish_reasons(single-element string array),gen_ai.response.id, and per-setgen_ai.request.{temperature,max_tokens,top_p,seed}(only set fields — absence is meaningful per §5.5.2). The existingopenarmature.llm.*attribute set is preserved alongside; both namespaces emit. Default-on (disable_genai_semconv: bool = False); opt out when an external auto-instrumentation library (OpenInference, opentelemetry-instrumentation-openai, etc.) is the canonical source of GenAI attributes for your stack. OTelObserver(resource=...)constructor argument. Optionalopentelemetry.sdk.resources.Resourcepassed to the privateTracerProvider. Lets callers setservice.name/service.versiondirectly rather than viaOTEL_SERVICE_NAME/OTEL_RESOURCE_ATTRIBUTESenvironment variables (which had to be set BEFORE constructing the observer to take effect — a footgun the explicit kwarg avoids).- Multi-processor support on
OTelObserver. Thespan_processorconstructor argument now accepts aSpanProcessor | Sequence[SpanProcessor]. Multi-destination export (e.g., HyperDX + Langfuse on one observer) becomes a one-line constructor call instead of a per-serviceCompoundSpanProcessorworkaround. OTelObserver(attribute_enrichers=...)hook. Sequence ofCallable[[Span, NodeEvent | None], None]invoked just before the observer ends each span. Lets users add backend-specific attributes (customlangfuse.*keys, vendor span kinds, etc.) without subclassing or mutatingspan._attributespost-on_end. The event isNoneon synthetic close sites (subgraph dispatch, detached root, fan-out instance, invocation span, shutdown drain); enrichers that need per-event context short-circuit onNone. Exceptions are caught and warned, never propagated to the dispatch worker.OTelObserver(payload_max_bytes=...)truncation cap. Per-attribute byte cap for the §5.5.1 payload attributes. Default 65,536 (64 KiB) per attribute; minimum 256 bytes (rejected at construction). The truncation algorithm (spec §5.5.5) emits the largest UTF-8 code-point-aligned prefix that fits withincap - len(marker)bytes followed by the marker…[truncated, M bytes total]. Inline image bytes are unconditionally redacted at the provider before any cap applies (see Image redaction below).OpenAIProvider(genai_system="openai")constructor argument. Default"openai"; override for non-OpenAI endpoints that speak the OpenAI Chat Completions wire format (vLLM, LM Studio, llama.cpp, sglang). Surfaces as thegen_ai.systemspan attribute. No base-URL sniffing happens — the same host:port could be any of several servers, and a wrong inference is worse than the explicit opt-in.openarmature.observability.LLM_NAMESPACEandopenarmature.observability.LlmEventPayloadpublic exports. The("openarmature.llm.complete",)sentinel namespace used by the LLM-provider hook and the payload shape backend observers consume. Third-partyProviderimplementations can dispatch their own LLM events viacurrent_dispatch()(NodeEvent(..., namespace=LLM_NAMESPACE, pre_state=LlmEventPayload(...))); custom observers can recognize the same sentinel and read attributes off the payload. Previously private (_LLM_NAMESPACE,_LlmEventState); the old underscore-prefixed names are no longer exported.Response.response_idandResponse.response_modeltyped fields. Mirror the wire response'sidandmodelfields when the provider returns them. Surface asgen_ai.response.idandgen_ai.response.modelper spec §5.5.3; also useful for downstream cross-referencing with provider-side billing or audit logs without reaching intoResponse.raw.
- Prompt-context attribute propagation now survives the dispatch-worker task boundary. Previously the OTel observer read
current_prompt_result()/current_prompt_group()from inside_handle_llm_event, which runs in the engine's delivery-worker task.asyncio.create_task(deliver_loop(queue))snapshots the current Context at task creation, before any node body runs — so the ContextVars set bywith_active_prompt(...)were never visible to the worker.openarmature.prompt.*attributes silently went missing on the LLM span. Fixed by capturing both ContextVars at dispatch time inside theOpenAIProvider.complete()call (which runs in the node task, wherewith_active_promptIS active) and threading the snapshots through theLlmEventPayload. The observer reads from the payload, not the ContextVar. - Inline image bytes are redacted at the provider, not the observer. Image content blocks with
ImageSourceInlineare serialized withsourcereplaced by{type: "inline_redacted", byte_count: N}per §5.5.5 before the payload reaches the observability dispatch queue. Defense-in-depth: bytes never leave the provider in event form, so custom observers subscribing to the LLM event (enabled byLlmEventPayloadbeing public) cannot accidentally leak raw image bytes regardless of their implementation.media_typeanddetailare preserved at the image-block level per llm-provider §3.1.2. URL-form images pass through unchanged. OTelObserver.shutdown()docstring documents theBatchSpanProcessorflush gotcha. Under fast or unusual teardown orderings (e.g., FastAPI TestClient teardown that closes the event loop before the batch processor's export thread finishes), spans can appear dropped. Documented workarounds: callprovider.force_flush(timeout_millis=…)explicitly beforeshutdown(), or useSimpleSpanProcessorin tests.
- Pinned spec version bumped to v0.17.0. Per the additive-only governance rule (proposal 0024 adds; never renames), implementations passing v0.16.1 conformance fixtures continue to pass under v0.17.0; the new fixtures (012-021) add cases without modifying existing ones.
Docs-and-examples release. Pinned spec stays at v0.16.1; no proposals implemented this cycle. The focus was bringing the docs site, README, and examples up to par with the v0.6.0 implementation and filling reference-doc gaps that mkdocstrings was silently dropping.
openarmature.graph.NextCallandopenarmature.graph.default_classifierexports. Promoted from theopenarmature.graph.middlewaresubmodule.NextCallis the Protocol describing thenext_callable a middleware receives;default_classifieris the retry classifier's default predicate (matchescategoryagainstTRANSIENT_CATEGORIES). Users writing custom middleware can type theirnext_parameter and extend the default classifier without reaching into the submodule.- Middleware concept page. New
docs/concepts/middleware.mdcovering the protocol shape, four registration sites (per-node, per-graph, per-branch, per-fan-out-instance), composition order, subgraph boundary, error semantics, and the built-inRetryMiddlewareandTimingMiddleware. - Complete reference docs. Added docstrings to 35 previously-undocumented public members across
graph,prompts, andcheckpoint. mkdocstrings silently omits entries without a docstring, which meant the most fundamental builder methods (add_node,add_edge,set_entry,compile) and the entireCheckpointerbackend method surface were invisible in the rendered reference. Every name in each subpackage's__all__now renders. - Examples 05–09. New examples covering fan-out with retry, parallel branches, multimodal prompts, checkpointing with state migration, and tool use. Per-example docs pages with mermaid diagrams under
docs/examples/. Examples 00–04 were scrubbed and standardized for consistency with the new set. RELEASING.md. Documents the rc-first release flow (TestPyPI then PyPI), the tag-name dispatch rules, the pre-release checklist, rc iteration, and rollback via PyPI yank.- Docs site UX, nav, and reference cleanup. Sweep of nav structure, internal links, and reference page organization to match the v0.6.0 surface.
FanOutNode.runandParallelBranchesNode.runraiseNotImplementedErrorinstead ofRuntimeError. Both methods exist only to satisfy theNodeprotocol; the engine dispatches these node types throughrun_with_context.NotImplementedErroris the right signal and stays backwards-compatible since it subclassesRuntimeError(existingexcept RuntimeErrorcatches still work).
- Pinned spec version unchanged at v0.16.1. No proposals landed this cycle; the release is docs- and examples-focused. The next functional release will resume with new spec proposals.
Consolidated release for the five-PR batch: structured output (proposal 0016), image content blocks (proposal 0015), prompt management (proposal 0017), state migration for checkpoints (proposal 0014), and parallel branches (proposal 0011). Pinned spec jumps from v0.10.0 to v0.16.1.
- Parallel branches (proposal 0011, introduced in spec v0.11.0; attempt-index propagation clarified in spec v0.16.1). New
GraphBuilder.add_parallel_branches_node(name, *, branches, error_policy, errors_field, middleware)surface dispatches M heterogeneous compiled subgraphs concurrently per pipeline-utilities §11.BranchSpec(subgraph + inputs/outputs projection + branch middleware) andParallelBranchesNodetypes exported fromopenarmature.graph. Branch insertion order determines fan-in merge order regardless of completion timing (§11.8). Two error policies:"fail_fast"raisesParallelBranchesBranchFailed(aNodeExceptionsubtype) withbranch_name, original cause as__cause__, andrecoverable_statecarrying the parent's pre-dispatch snapshot — no buffered branch contributions are visible (§11.5 buffer-and-apply)."collect"records per-branch failures in an optionalerrors_field(each record carriesbranch_name+category+ implementation-defined extras) and continues. Two new error categories:ParallelBranchesNoBranches(compile time, empty branches map) andParallelBranchesBranchFailed(runtime, fail_fast branch raise). NodeEvent.branch_name: str | None(proposal 0011 / graph-engine §6). Populated on events from nodes inside a parallel-branches branch, absent outside. Independent offan_out_index— both may be present simultaneously when a branch contains a fan-out (or a fan-out instance contains a parallel-branches node). The combined(namespace, branch_name, fan_out_index, attempt_index, phase)tuple is the event-source uniqueness key.openarmature.branch_nameOTel span attribute. Mirrors the existingopenarmature.node.fan_out_index. Emitted on synthesized inner-node spans whenbranch_nameis populated on the event. The two attributes coexist on inner nodes of a fan-out-inside-a-branch composition.- Attempt-index ContextVar propagation through transitive retry (graph-engine §6 v0.16.1). Retry middleware now sets the
attempt_indexContextVar before eachnextcall; the engine readscurrent_attempt_index()when emitting events. This makes retry semantics symmetric across direct (per-node middleware) and transitive (instance / branch / fan-out instance_middleware) wrapping — events from inner nodes of a subgraph the retry re-invokes carry the wrapping retry's counter, not a freshly-zeroed inner counter. Innermost-wins precedence falls out of Python's ContextVar set/reset token stack. Pre-existing node-level retry behavior is unchanged. - State migration for checkpointed graphs (proposal 0014, introduced in spec v0.15.0; refined by proposal 0018 in spec v0.16.0). Saved checkpoints whose
schema_versiondoesn't match the current state class now route through a registered migration chain instead of failing on resume. Surface:State.schema_version: ClassVar[str] = ""(declare a non-empty value to opt in),GraphBuilder.with_state_migration(from_version, to_version, migrate)andwith_state_migrations(*migrations)for registration,StateMigrationandMigrationRegistrytypes exported fromopenarmature.checkpoint. Chain resolution is BFS over the registered edges; the shortest path wins. Three new error categories:CheckpointStateMigrationChainAmbiguous(proposal 0018: duplicate(from, to)pair at registration time, or multiple distinct shortest paths between the saved and current versions at resume time),CheckpointStateMigrationMissing(no chain bridges the versions), andCheckpointStateMigrationFailed(a migration function raised). All non-transient. Post-migration deserialization failures still route toCheckpointRecordInvalidper §10.12.4. The same chain applies to each entry inparent_statesin lockstep with the outer state per §10.12.2. Routing precedence per §10.10 (v0.16.0): chain-ambiguous → missing → failed → record-invalid. Checkpointer.supports_state_migrationProtocol attribute. Marks whether a backend can expose the structural intermediate form (a plain dict, JSON tree) the migration registry consumes.SQLiteCheckpointer(serialization="json")opts in;SQLiteCheckpointer(serialization="pickle")andInMemoryCheckpointeropt out. On version mismatch against a non-migration-eligible backend the engine raisesCheckpointRecordInvalidper spec §10.12.1.openarmature.checkpoint.migrateOTel span (proposal 0014 §6 cross-ref). Versioned resumes whose migration chain runs emit a zero-durationopenarmature.checkpoint.migratespan on the OTel observer, parented under the invocation root span. Attributes:openarmature.checkpoint.migrate.from_version,openarmature.checkpoint.migrate.to_version(the final target),openarmature.checkpoint.migrate.chain_length. The §10.12.3 fast path (versions match, registry not consulted) emits no span. Engine-side: a syntheticcheckpoint_migratedobserver phase carries a_MigrationSummarypayload from_migrate_recordthrough to the OTel observer; the new phase is gated off default subscriptions (observers opt in explicitly viaphases={..., "checkpoint_migrated"}).- Prompt-management capability (proposal 0017, introduced in spec v0.15.0). New
openarmature.promptssubpackage.PromptManagercomposes one or morePromptBackends, exposesfetch/render/get, applies the §8 fallback semantics (prompt_store_unavailablecontinues to the next backend;prompt_not_foundstops the chain), and renders templates with Jinja2'sStrictUndefinedper §7.Prompt/PromptResult/PromptGroupare Pydantic models matching spec §3 / §4 / §9. Three error categories (PromptNotFound,PromptRenderError,PromptStoreUnavailable) withPROMPT_TRANSIENT_CATEGORIESexported for retry-middleware classifiers.FilesystemPromptBackendis the minimum local-filesystem reference backend (layout:<root>/<label>/<name>.j2;versionderived from the first 16 hex chars oftemplate_hash). New runtime dependency:jinja2>=3.1. openarmature.prompts.context— observability propagation per spec §11.with_active_prompt(result)andwith_active_prompt_group(group)context managers +current_prompt_result()/current_prompt_group()inspectors. When the OTel observer is active and an LLM call fires insidewith_active_prompt, theopenarmature.llm.completespan carries the normativeopenarmature.prompt.*attributes (name,version,label,template_hash,rendered_hash,group_name). Nesting is innermost-wins.- Image content blocks for user messages (proposal 0015, introduced in spec v0.13.0).
UserMessage.contentnow acceptsstr | list[ContentBlock]. The block surface introducesTextBlock,ImageBlock,ImageSourceURL,ImageSourceInline, and theContentBlock/ImageSourcediscriminated unions over the block / sourcetypefield.ImageBlockcarries amedia_type(required for inline sources; ignored for URL sources; typed asstr | Noneso callers MAY pass anyimage/*type the bound model supports) and an optionaldetailhint ("auto"/"low"/"high";Nonedefault omits the field from the wire so providers apply their own default). System, assistant, and tool messages stay text-string-only; image inputs are user-only in v1. OpenAIProvidercontent-array wire mapping. WhenUserMessage.contentis a content-block sequence, the wire body uses OpenAI'scontentarray per §8.1.1.TextBlock → {type: "text", text}.ImageBlockwith a URL source maps to{type: "image_url", image_url: {url, detail?}}.ImageBlockwith an inline source constructs an RFC 2397data:<media_type>;base64,<base64_data>URI and goes through the sameimage_urlentry shape. Inline bytes pass through unchanged — no inspection, transcoding, or re-encoding.- New error category
ProviderUnsupportedContentBlock(non-transient). Raised when the bound model rejects a content block type / media variant. Distinct fromProviderInvalidRequest(which covers spec-shape malformation): this category surfaces a capability mismatch, letting callers route differently (e.g., fall back to a multimodal-capable provider) without overloading the malformed-request category. Carriesblock_type("image" / "audio" / "video") andreason(provider's human-readable message) when those are recoverable from the rejection.OpenAIProviderdetects content rejection via HTTP 400 bodies — heuristic onerror.code(known set:image_content_not_supported,unsupported_image_media_type,audio_content_not_supported, etc.),error.type(image_parse_error), anderror.message("does not support" + image/audio/video). - Structured output (proposal 0016, introduced in spec v0.14.0).
Provider.complete()now accepts an optionalresponse_schemaparameter — either a JSON Schema dict or a PydanticBaseModelsubclass. When supplied, the provider constrains the model's output to the schema and populatesResponse.parsedwith the validated value (dictfor dict-schema input, aBaseModelinstance for class input). NewStructuredOutputInvaliderror category (non-transient by default) raises on JSON parse failure or schema validation failure; carries the requested schema, the raw response content, and a failure description. OpenAIProvidernative response_format wire path. Whenresponse_schemais supplied, the chat-completions request body carriesresponse_format: { type: "json_schema", json_schema: { name, schema, strict } }. Thestrictflag is determined by a deep recursive walk over the schema (object-property required-coverage rule acrossanyOf/oneOf/allOfand$reftargets, with cycle protection); unresolvable refs fall through tostrict: false. Thenamefield usesschema.titlewhen present, otherwise a deterministic sha256-prefix hash.OpenAIProviderprompt-augmentation fallback. Constructor flagforce_prompt_augmentation_fallback: bool(defaultFalse) and read-only inspect propertyuses_prompt_augmentation_fallback: bool. When the flag is on, structured-output calls build a fresh message list with a system directive containing the serialized schema, omitresponse_formatfrom the wire, and validate the response post-receive. The caller's originalmessageslist is never mutated. Use for OpenAI-compatible servers (older vLLM, some LM Studio releases, llama.cpp variants) that reject or silently ignoreresponse_format.- Provider-agnostic schema helpers.
openarmature.llm.validate_response_schema(schema)(raisesProviderInvalidRequestwhen the schema is not a dict with a top-leveltype: "object") andopenarmature.llm.strict_mode_supported(schema)(the deep-tree strict-mode constraint check) are exported for reuse by future Anthropic/Gemini providers. - Capability-agnostic conformance harness helpers.
tests/conformance/harness/wire.pyaddsmatch_wire_body(recursive deep-equal with"*"wildcard support),assert_response_format_absent,assert_system_references_schema, andassert_error_carriesfor theexpected_wire_request[_checks]andexpected.raises.carries.{...}fixture shapes. Used by the 0016 fixtures; available for the upcoming 0014 / 0015 / 0017 fixture sets. - Runtime dependency:
jsonschema>=4.0. Used by the dict-schema validation path. The Pydantic-class path uses Pydantic's native validator and does not needjsonschema.
- Pinned spec version: 0.10.0 → 0.16.1. Adopts the skip-ahead governance principle: the submodule jumps across v0.11.0–v0.16.1 (proposals 0009, 0011, 0014, 0015, 0016, 0017, 0018) in one bump. All five proposals (0011, 0014, 0015, 0016, 0017) are implemented in the batch's release; the v0.16.1 clarification of attempt-index propagation through transitive retry middleware lands with the proposal 0011 implementation.
CheckpointRecord.schema_versionsemantic shift (proposal 0014). Previously a backend-internal record-shape version (CHECKPOINT_SCHEMA_VERSION = "1"constant), now the user-facing state-schema version per spec §10.2. The framework readstype(state).schema_versionat save time. Pre-PR-4 records carrying"1"are reinterpreted as user-facing v1 identifiers; users with such records either declareschema_version="1"on their state class or discard the pre-PR-4 records.SQLiteCheckpointerno longer rejects records with non-defaultschema_versionat the backend boundary; version-mismatch routing is now an engine concern at resume time. TheCHECKPOINT_SCHEMA_VERSIONmodule constant is removed; future record-shape evolution can add backend-private metadata fields if needed.NodeEvent.pre_statetypedAny(wasState). Required by the newcheckpoint_migratedphase which carries a_MigrationSummarypayload rather than aStateinstance. Observer authors who type-narrowedpre_statetoStateshould treat it asAnyand narrow per-phase (e.g.,if event.phase == "completed": ...). Thecheckpoint_savedphase already carried a State-flavored shape (not necessarily a typedStatesubclass instance), so this widens the declared type to match runtime reality rather than introducing a new constraint.
-
Pre-1.0 MINOR. Two behavioral changes ship in this release:
- Retry-MW attempt-index propagation. Events from inner nodes of a subgraph wrapped by retry middleware (branch middleware, fan-out
instance_middleware, or any retry on a wrapping subgraph) now carry the wrapping retry's attempt counter on each re-invocation rather than starting at 0. Per-node retry behavior is unchanged. Matches spec v0.16.1's clarification of the graph-engine §6 contract. CheckpointRecord.schema_versionsemantic shift. Previously a backend-internal record-shape version (the removedCHECKPOINT_SCHEMA_VERSION = "1"constant), now the user-facing state-schema version per spec §10.2. Pre-v0.6.0 records carrying"1"are reinterpreted as user-facing v1 identifiers; declareschema_version="1"on the corresponding state class or discard the records.
Existing callers who don't wrap subgraphs in retry middleware and don't declare a state-schema version see no behavior change.
- Retry-MW attempt-index propagation. Events from inner nodes of a subgraph wrapped by retry middleware (branch middleware, fan-out
First release on real PyPI. Catches the implementation up from spec v0.5.x to v0.10.0 across six phases — the spec accepted eight proposals while the python lib was at v0.3.1, and v0.5.0 lands all of them in one curated drop.
- Typed conformance harness (Phase 0). Single parametrised test target driving all 68 spec fixtures under discriminated-union YAML parsers. Replaces the earlier hand-rolled per-fixture wiring.
- Observer pair model (Phase 1, spec v0.6.0 / proposal 0005 §6).
ObserverProtocol (async callable),SubscribedObserverwith phase subscription set ({"started", "completed", "checkpoint_saved"}),RemoveHandle.remove(), and a serial delivery queue per spec §6 ordering. Observer exceptions don't propagate; reported viawarnings.warn. - Middleware (Phase 2, proposal 0004).
MiddlewareProtocol with the canonical(state, next) → partial_updateshape,compose_chainruntime, and five stdlib middlewares:RetryMiddleware,TimingMiddleware,ErrorRecoveryMiddleware,ShortCircuitMiddleware,TraceRecorderMiddleware. Per-graph and per-node middleware composition. - Fan-out runtime (Phase 3, proposal 0005 pipeline-utilities side).
FanOutNodefor parallel fan-out over anitems_fieldor acount(int or callable resolver). Configurable concurrency, error policy (fail_fast/collect),inputs/extra_outputsprojection, optionalerrors_fieldcollection. Composes with retry middleware on the fan-out node and on per-instance subgraphs. - LLM provider (Phase 4, proposal 0006). New
openarmature.llmpackage:ProviderProtocol withready()/complete(messages, tools=None, config=None);OpenAIProvider(HTTPX-based, OpenAI-compatible wire); typedMessage/ToolCall/Tool/Response/RuntimeConfig; seven error categories (ProviderAuthentication,ProviderUnavailable,ProviderInvalidRequest,ProviderInvalidResponse,ProviderInvalidModel,ProviderModelNotLoaded,ProviderRateLimitwithretry_after). Tool-call ids preserved verbatim through the wire. - Checkpointing (Phase 5, proposal 0008).
CheckpointerProtocol (save/load/list/delete) withCheckpointRecordandNodePositionshapes;InMemoryCheckpointerreference impl;CheckpointNotFound/CheckpointRecordInvalid/CheckpointSaveFailederror categories;checkpoint_savedobserver phase; resume-from-checkpoint semantics for fan-out and subgraph compositions. - Observability / OTel (Phase 6, proposal 0007).
OTelObservermapping observer events → OpenTelemetry spans with privateTracerProvider(no global pollution); §4.4 detached subgraph + detached fan-out trace mode; §5.5 LLM-provider span emission withdisable_llm_spansopt-out; §5.6 cross-cuttingopenarmature.correlation_idon every span; §10.8checkpoint_savedzero-duration span.install_log_bridgewires the stdlib root logger through OTel's Logs Bridge (deprecation-aware viaopentelemetry-instrumentation-logging) so log records emitted within an invocation carry the active span'strace_id/span_idplusopenarmature.correlation_id.prepare_syncsynchronous observer hook so logs emitted on the FIRST line of a node body (before anyawait) pick up the right span. Fan-out per-instance dispatch span synthesis (§5.4) withparent_node_namecached and applied per-instance. current_correlation_id()public API. Read the per-invocation cross-backend join key from anywhere within the invocation's async call tree.- Subgraph configuration plural form. Builder accepts
subgraphs:alongsidesubgraph:for fixture compatibility.
- Pinned spec version: 0.5.x → 0.10.0. Lands proposals 0004 (middleware), 0005 (fan-out + observer pair model), 0006 (llm-provider), 0007 (observability/OTel), 0008 (checkpointing), 0011 (
prepare_synchook), 0012 (completedevent after edge eval), 0013 (fan_out_configonNodeEvent). - Edge-resolution failures share the preceding node's event pair (spec v0.9.0 / proposal 0012).
routing_errorandedge_exceptionpopulateerroron the preceding node'scompletedevent withpost_state=Noneinstead of producing a separate pair. All five §4 runtime error categories now land via the same uniform mechanism. - Observer protocol contract. Async-only callable; phase-filtered delivery via
SubscribedObserver.phases; serial single-task delivery worker; observer errors isolated viawarnings.warn.
- Log bridge filter placement. Phase 6.0's
_CorrelationIdFilterlived on the root logger; Python's logging propagation walks ancestor handlers but not ancestor filters, so child-logger records (the normallogging.getLogger("module")pattern) were missed. Replaced with a process-globalLogRecordfactory that fires uniformly at record construction. - OTelObserver concurrency-safe state scoping. Per-invocation span state now keyed by
invocation_idso concurrent invocations sharing one observer instance don't collide on the in-flight span maps. - Spec submodule pin sync. Internal
spec_versionmatched the submodule HEAD across phase boundaries; tracked viatests/test_smoke.py.
- First real PyPI publish. Pre-release verification continues to flow through TestPyPI per
docs/RELEASING.md. ThepypiGitHub Environment requires a manual approval click before any real-PyPI upload — keep it on. - Pre-1.0 SemVer. Behavioral changes may land in MINOR bumps. Several Phase 1+ contracts changed shape vs. v0.4.0 — most user-visible: the observer pair model in Phase 1, the edge-resolution failure mechanism in Phase 6.1.
- Cross-language posture. This release tracks spec v0.10.0; the OpenArmature TypeScript implementation will land separately under the same spec.