From 6b411b51b0bdd31c348e2fe29eab48f3cbf92897 Mon Sep 17 00:00:00 2001 From: enyst Date: Sat, 14 Feb 2026 06:19:10 +0000 Subject: [PATCH] docs(sdk): add deprecation & API stability guide Document the three deprecation policies enforced by CI: - Deprecation-before-removal (check_sdk_api_breakage.py) - MINOR version bump for breaking changes - Event field deprecation (permanent handlers) Covers the canonical helpers (@deprecated, warn_deprecated), the step-by-step removal workflow, and which packages are covered. Co-authored-by: openhands --- docs.json | 1 + sdk/guides/deprecation-policy.mdx | 143 ++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 sdk/guides/deprecation-policy.mdx diff --git a/docs.json b/docs.json index 16aa34c5..35f36e04 100644 --- a/docs.json +++ b/docs.json @@ -278,6 +278,7 @@ "sdk/guides/agent-delegation", "sdk/guides/iterative-refinement", "sdk/guides/security", + "sdk/guides/deprecation-policy", "sdk/guides/metrics", "sdk/guides/observability", "sdk/guides/secrets", diff --git a/sdk/guides/deprecation-policy.mdx b/sdk/guides/deprecation-policy.mdx new file mode 100644 index 00000000..e8f06c4b --- /dev/null +++ b/sdk/guides/deprecation-policy.mdx @@ -0,0 +1,143 @@ +--- +title: Deprecation & API Stability +description: How the SDK manages deprecations, version bumps, and backward compatibility. +--- + +The OpenHands SDK enforces a structured deprecation lifecycle to keep the public +API stable while allowing it to evolve. Two CI checks run automatically on every +release to enforce these policies. + +## Public API Surface + +Each published package defines its public API via `__all__` in its top-level +`__init__.py`. The following packages are currently covered: + +| Package | Distribution | `__all__` | +|---------|-------------|-----------| +| `openhands.sdk` | `openhands-sdk` | ✅ | +| `openhands.workspace` | `openhands-workspace` | ✅ | + + +`openhands-tools` does not yet define `__all__` and is not covered by the +automated breakage checks. This is tracked in +[#2074](https://github.com/OpenHands/software-agent-sdk/issues/2074). + + +## Deprecation Helpers + +The SDK provides two canonical ways to mark something as deprecated, both in +[`openhands.sdk.utils.deprecation`](/sdk/api-reference/openhands.sdk.utils): + +### `@deprecated` decorator + +Use on classes and functions that will be removed in a future release: + +```python +from openhands.sdk.utils.deprecation import deprecated + +@deprecated(deprecated_in="1.10.0", removed_in="1.12.0") +class OldThing: + ... +``` + +### `warn_deprecated()` function + +Use for runtime deprecation warnings on dynamic access paths (e.g., property +accessors, conditional branches): + +```python +from openhands.sdk.utils.deprecation import warn_deprecated + +class MyModel: + @property + def old_field(self): + warn_deprecated("MyModel.old_field", deprecated_in="1.10.0", removed_in="1.12.0") + return self._new_field +``` + +Both helpers emit a `DeprecationWarning` so users see the message during +development, and record metadata that CI tooling can detect. + +## Policy 1: Deprecation Before Removal + +**Any symbol removed from a package's `__all__` must have been marked as +deprecated for at least one release before removal.** + +This is enforced by `check_sdk_api_breakage.py`, which AST-scans the +*previous* PyPI release looking for `@deprecated` decorators or +`warn_deprecated()` calls. If a removed symbol was never deprecated, +CI flags it as an error. + + + +Add `@deprecated(...)` or `warn_deprecated(...)` in the current release. +The symbol stays in `__all__` and continues to work — users just see a warning. + + +The deprecation is now recorded in the published package on PyPI. + + +Remove it from `__all__` (and the code). CI will verify the prior release had +the deprecation marker and allow the removal. + + + +## Policy 2: MINOR Version Bump for Breaking Changes + +**Any breaking change — removal of an exported symbol or structural change to a +public class/function — requires at least a MINOR version bump** (e.g., +`1.11.x` → `1.12.0`). + +This applies to all structural breakages detected by +[Griffe](https://mkdocstrings.github.io/griffe/), including: +- Removed symbols from `__all__` +- Removed attributes from exported classes +- Changed function signatures + +A PATCH bump (e.g., `1.11.3` → `1.11.4`) with breaking changes will fail CI. + +## Event Field Deprecation (Special Case) + +Event types (Pydantic models used in event serialization) have an additional +constraint: **old events must always load without error**, because production +systems may resume conversations containing events from older SDK versions. + +When removing a field from an event type: + +1. **Never use `extra="forbid"` without a deprecation handler** — old events + containing removed fields would fail to deserialize. +2. **Add a permanent model validator** using `handle_deprecated_model_fields`: + +```python +from openhands.sdk.utils.deprecation import handle_deprecated_model_fields + +class MyEvent(BaseModel): + model_config = ConfigDict(extra="forbid") + + _DEPRECATED_FIELDS: ClassVar[tuple[str, ...]] = ("old_field_name",) + + @model_validator(mode="before") + @classmethod + def _handle_deprecated_fields(cls, data: Any) -> Any: + return handle_deprecated_model_fields(data, cls._DEPRECATED_FIELDS) +``` + + +Deprecated field handlers on events are **permanent** and must never be removed. +They ensure old conversations can always be loaded regardless of when they were +created. + + +## CI Checks + +Two scripts enforce these policies automatically: + +| Script | Runs on | What it checks | +|--------|---------|---------------| +| `check_sdk_api_breakage.py` | Release PRs (`rel-*` branches) | Deprecation-before-removal + MINOR bump | +| `check_deprecations.py` | Every PR | Deprecation deadline enforcement | + +Together they ensure that: +- Users always get advance warning before APIs are removed +- Breaking changes are properly versioned +- Deprecated code is eventually cleaned up