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.
- 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.
- Release gate cleared with PR-5 (proposal 0011). All five proposals in the batch ({0011, 0014, 0015, 0016, 0017}) are now implemented. Tag the consolidated release once this PR merges.
- Pre-1.0 MINOR. Existing free-form callers (no
response_schema) see no behavior change — the new field defaults toNone, the wire body omitsresponse_format, andResponse.parsedremains absent.
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.