# Versioning & stability

> What a release may change, how stored and wire formats are versioned, and what happens when a run resumes on changed code.

Source: https://docs.pyrula.io/getting-started/stability/

[Semantic Versioning](https://semver.org/). The packages (`pyrula`, `pyrula-workflows`,
`pyrula-agents`, `pyrula-kafka`, `pyrula-cloud-client`) release together; install matching
minors. Current gaps: [Known limitations](https://docs.pyrula.io/getting-started/known-limitations.md).

## Releases before 1.0

- A minor release (`0.4` to `0.5`) may change or remove a public name or a stored format.
  Changes are in the [release notes](https://github.com/pyrula-io/pyrula/releases), and a
  removed name is deprecated first.
- A patch release (`0.4.1` to `0.4.2`) holds fixes and additions only: no removed or renamed
  public names, changed return shapes, or changed stored or wire formats.

Pin a minor (`pyrula~=0.4.0`) for a stable target.

## Supported surface

Each runtime package, and `pyrula.workflows.testing`, classifies its exports in a `STABILITY`
map:

```python
from pyrula.workflows import STABILITY

STABILITY["RunEvent"]              # "stable"
STABILITY["InlineWorkflowRunner"]  # "stable"
```

- `"stable"`: what you write against: decorators, contexts, clients, exceptions, the concrete
  stores.
- `"extension"`: what you implement: the store ABCs, `LLMClient`, `MemoryBackend`,
  `ScheduleBackend`, the contract kits. Signature changes go through deprecation. See
  [Extension points](https://docs.pyrula.io/reference/extension-points.md).
- `"deprecated"`: still works, being removed.

A name not in `__all__`, or with a leading underscore, is internal. There is no beta tier.
`pyrula.parallel` (`par_map` and its errors) is supported too.

Guarantees per surface: [adapters](https://docs.pyrula.io/agents/integrations/guarantees.md),
[Kafka client](https://docs.pyrula.io/kafka/guarantees.md), [Kafka runtime](https://docs.pyrula.io/workflows/guides/kafka-guarantees.md).

## Deprecations

A stable or extension name is removed in two steps: one minor keeps it working and raises
`DeprecationWarning` naming the removing release, and the next removes it. Run tests with
`-W error::DeprecationWarning` to catch these early.

## Stored and wire formats

What the current release does when it meets data in another version of a format:

| Format | Version marker | Older data | Newer data |
|---|---|---|---|
| Run journal (the event log in every store) | `EVENT_SCHEMA_VERSION`, currently `"1"`, stamped on each event, plus the `requires` and `encoding` event headers | Read as-is; `"1"` is the only version so far | A worker hands the run back, unrun and uncounted, when a journal holds anything it cannot read (see below) |
| Postgres archive, agent projection and agent memory | A one-row `*_schema_compat` table per schema (generation, oldest reader generation, baseline revision), written by its migrations | Used as-is at or above the oldest generation this code supports; a lower one, or a different baseline, fails with `ArchiveSchemaMismatch` (recreate or migrate) | Used as-is while its oldest reader generation is at or below the code's; otherwise `ArchiveSchemaMismatch` says to upgrade pyrula |
| Case bundle | `format` in the manifest, currently `1` | Read up to the reader's own format | `format_unsupported` only when the format number is newer; unknown keys are kept and an unknown call kind is skipped and reported |
| Cloud outbox (the connector's local SQLite spool) | `OUTBOX_FORMAT_VERSION = 4`, in SQLite's `user_version` | Migrated forward in place when the spool opens, keeping its events | The connector refuses to start with `OutboxFormatTooNew` and leaves the file untouched; upgrade the SDK or move the file aside |
| Valkey key layout | None | Not checked | Not checked |
| Cloud wire (`/api/v1`) | The path prefix and `wire_version` (currently `1`) in the ingest and register bodies | Pyrula Cloud parses every version it still supports | A 422 `wire_version_unsupported` answer is retried with backoff and the events stay queued |

Within `/api/v1` the event vocabulary only grows: event kinds, run statuses and error codes
are not removed or renamed.

The Valkey key layout is not versioned. When the release notes list a layout change, drain
in-flight runs before upgrading or point the new release at a fresh namespace
(`PYRULA_VALKEY_NAMESPACE`). Two builds can share a server during a rolling deploy.

### Journals from a newer release

During a rolling deploy an older worker refuses a run whose journal holds:

- an unknown event kind in a framework namespace (`run:`, `step:`, `llm:`, `block:`, `tool:`,
  `agent:`, `context:`, `stream:`, `child:`, `select:`, `mcp:`, `saga:`, `approval:`, or the
  bare `heartbeat` and `patch_marker`);
- an unknown `step_kind` on `step:pending`, `step:done` or `step:error`;
- an unknown feature in an event's `requires` header;
- an `encoding` header other than `json`;
- a `schema_version` major higher than its own.

Event kinds outside those namespaces are yours (from `ctx.emit`) and are skipped. A writer can
mark a new kind or `step_kind` `ignorable: "1"`; features, encodings and schema majors are
never skippable.

The refused run goes back to pending with no event written, no attempt counted and no failure.
The worker logs `run_requires_newer_runtime`, counts it under `needs_newer_runtime`, and skips
that run for 60 seconds, and a newer worker claims it. Rolling back leaves such runs queued
until the newer release returns.

## Resuming a run after its code changed

A run is stamped with its workflow's version at submit time, which decides what a worker on
changed code does ([versioning guide](https://docs.pyrula.io/workflows/guides/versioning.md)).

- **No `version=`:** the version is a hash of the body and the policy is `auto_upgrade`. The
  worker replays history against the edited body. A compatible edit finishes the run; an
  incompatible one (renamed, removed or reordered step, changed inputs) ends it with
  `run:error` code `incompatible_upgrade`.
- **`version=` changed:** policy `pinned`. Only a worker on that version claims the run. With
  `stranded_policy=StrandedPolicy.FAIL` it ends with `no_compatible_worker` after
  `stranded_timeout`.
- **Body changed, `version=` kept:** unsupported. The worker trusts the label. A step with
  different inputs ends the run with `replay_input_changed`; a renamed or removed step is not
  detected and its recorded result is dropped. Change `version=` whenever an edit touches a
  step.

`@agent` takes the same options. A failed resume makes `result()` raise `DurableInvocationError`.
