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/faq.mdx b/sdk/faq.mdx index 5abcd038..f5f573ad 100644 --- a/sdk/faq.mdx +++ b/sdk/faq.mdx @@ -269,6 +269,14 @@ This encourages the agent to use the finish tool rather than asking for confirma For the full implementation used in OpenHands benchmarks, see the [fake_user_response.py](https://github.com/OpenHands/benchmarks/blob/main/benchmarks/utils/fake_user_response.py) module. + +## How does deprecation work in the SDK? + +The SDK defines its public surface via `__all__` and uses the helpers in +[`openhands.sdk.utils.deprecation`](/sdk/api-reference/openhands.sdk.utils) to +emit deprecation warnings and cleanup deadlines. For the full lifecycle, see the +[Deprecation & API Stability](/sdk/guides/deprecation-policy) guide. + ## More questions? If you have additional questions: diff --git a/sdk/guides/deprecation-policy.mdx b/sdk/guides/deprecation-policy.mdx new file mode 100644 index 00000000..f08d1035 --- /dev/null +++ b/sdk/guides/deprecation-policy.mdx @@ -0,0 +1,163 @@ +--- +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 enforce these policies: +`check_sdk_api_breakage.py` on release PRs and `check_deprecations.py` on every PR. + +## 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` | Yes | +| `openhands.workspace` | `openhands-workspace` | Yes | +| `openhands.tools` | `openhands-tools` | Yes | + +The curated `__all__` list is the contract that `check_sdk_api_breakage.py` uses +to detect breaking changes. + +## Deprecation Helpers + +The SDK provides canonical helpers in +[`openhands.sdk.utils.deprecation`](/sdk/api-reference/openhands.sdk.utils) for +both deprecations and cleanup deadlines: + +### `@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 +``` + +### `warn_cleanup()` function + +Use for temporary workarounds that must be removed by a specific version or date: + +```python +from openhands.sdk.utils.deprecation import warn_cleanup + +warn_cleanup( + "Temporary workaround for upstream issue", + cleanup_by="1.16.0", +) +``` + +`@deprecated` and `warn_deprecated()` emit deprecation warnings and record +metadata that CI tooling can detect. `warn_cleanup()` emits a `UserWarning` once +its cleanup deadline is reached and is enforced by `check_deprecations.py`. + +## Policy 1: Deprecation Before Removal + +**Any symbol removed from a package's `__all__`, or any public member removed +from an exported class, 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. Deprecating a class counts as deprecating its +public members for the purposes of member removal. + + + +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 + cleanup deadline enforcement | + +`check_deprecations.py` scans `openhands-sdk`, `openhands-tools`, +`openhands-workspace`, and `openhands-agent-server` for expired `removed_in` +or `cleanup_by` deadlines. + + +Together they ensure that: +- Users always get advance warning before APIs are removed +- Breaking changes are properly versioned +- Deprecated code is eventually cleaned up