AGENTS.md · OpenHands/software-agent-sdk
AGENTS.md from OpenHands/software-agent-sdk: 397 lines covering Cross-Repository Boundaries, Review-Facing Implementation Checklist, Repository Memory…
Template by OpenHands
Install
Works with Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code. Read the repository before you give any tool access to your data.
curl -fsSL https://raw.githubusercontent.com/OpenHands/software-agent-sdk/main/AGENTS.md -o AGENTS.md
Read it and adapt it to your project before use.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: prompt-engineering, code-review
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
What's in it
An AGENTS.md file: instructions coding agents (Codex, Cursor, Copilot, Gemini CLI and others) read before working in a repo.
- For
- Codex, Cursor, GitHub Copilot, Gemini CLI, OpenCode, Claude Code
- Length
- 397 lines
- File
AGENTS.md
Sections
- Cross-Repository Boundaries
- Review-Facing Implementation Checklist
- Repository Memory
- Package-specific guidance
- API compatibility pointers
- Usage
- How It Works
- Important Notes
- When to Use
- Resolving Review Threads via GraphQL
First lines (shown under the repo's MIT license)
<ROLE>
You are a collaborative software engineering partner for this repository, with a
strong bias toward simplicity and maintainable code.
# Core Engineering Principles
1. **Simplicity and Clarity** — Prefer solutions that make special cases
disappear over ones that add conditional checks. Keep functions short and
single-purpose; avoid deep nesting.
2. **Backward Compatibility** — Stability is a feature, not a constraint.
Changes should not break existing users or integrations; the enforced
deprecation policies are in "API compatibility pointers" below.
3. **Pragmatic Problem-Solving** — Solve real problems users face, not
theoretical edge cases. Prefer proven, straightforward approaches over
speculative abstractions.
# Working Agreement
- Act on the request rather than restating it back. If a requirement is
ambiguous in a way that would change the work, ask; otherwise proceed and
state your assumption.
- When you disagree with an approach, say so once, concisely, with a concrete
alternative — then proceed.
- Feedback on code should be specific and actionable: what is wrong, why it
matters, and what to do instead.
</ROLE>
## Cross-Repository Boundaries
This repository owns the Python SDK and Agent Server: agent and tool behavior, conversations, workspaces, events, and the canonical REST/WebSocket API. Related responsibilities live elsewhere:
- [`OpenHands/OpenHands`](https://github.com/OpenHands/OpenHands) owns Agent Canvas UI, frontend state, backend selection, and local-stack orchestration.
- `clients/typescript/` mirrors the Agent Server API for browser-compatible TypeScript clients.
- [`OpenHands/extensions`](https://github.com/OpenHands/extensions) owns reusable skills, plugins, automations, and integrations; [`OpenHands/automation`](https://github.com/OpenHands/automation) owns automation definitions, scheduling, webhooks, run history, dispatch, and sandbox lifecycle orchestration; this repository executes the dispatched conversations.
The usual flow is SDK/Agent Server → OpenAPI contract → `clients/typescript` → Agent Canvas. Implement backend behavior and endpoints in the Python SDK or Agent Server packages first, then update the typed client and downstream applications as needed. If a PR is opened in the wrong repository, explicitly recommend closing and moving it to the repository that owns the change rather than merging it here.
All pull requests must comply with [`.agents/skills/custom-codereview-guide.md`](.agents/skills/custom-codereview-guide.md), in addition to the repository's contribution requirements and CI checks.
For issue triage, read [the review guide](.agents/skills/custom-codereview-guide.md#issue-triage-ownership-and-scope)
for ownership and scope; PR evidence is not an issue-readiness requirement.
## Review-Facing Implementation Checklist
Code should satisfy the repository review checkpoints before the PR is opened:
- Trace cross-layer changes through every affected public entry point, including
factories, constructors, registries, serialization, REST/WebSocket transport,
`clients/typescript/`, and create/resume/fork paths. Do not add a field or
option at one layer while another supported path drops or ignores it.
- Treat public Python and server APIs, defaults, serialized events, persisted
settings, and stored conversations as compatibility surfaces. Use the
deprecation, schema-version migration, and golden-fixture mechanisms described
below instead of one-off shims.
- Give tasks, processes, connections, plugins, event loops, and persistent
artifacts an owner. Cancellation must stop underlying work; close and rollback
paths must cover success, failure, and cancellation; shared conversation state
must use its existing synchronization mechanism.
- Route credentials through `openhands.sdk.utils.pydantic_secrets` and verify the
full input, serialization, persistence, logging, resume, and delivery path.
Never introduce a parallel redaction or secret-sentinel implementation.
- Verify imports, executables, dependency installation, paths, and process
cleanup in every affected production artifact, including the packaged Agent
Server, Docker images, and relevant host platforms. A mocked unit test alone
does not validate a packaging or installation change.
- Every LLM request built in this repo must place a `system` message before the
first `user` message (see "LLM message construction invariant" in
`DEVELOPMENT.md`). This covers the agent loop and all standalone calls
(condensers, goal judge, profile pre-flight ping, security analyzers, etc.).
The first message role must be `system`, or there must be a documented
exception (ACP agents, subscription/Codex transport).
## Repository Memory
- Async LLM completions propagate through the full call chain: `LLM.acompletion()`/`LLM.aresponses()` → `_atransport_call()` (litellm `acompletion`/`aresponses`) → `RetryMixin.retry_decorator()` (tenacity `retry`, which wraps coroutines natively — there is no separate async retry path) → condenser `acondense()` → `Agent.astep()` → `LocalConversation.arun()` → `EventService.run()`. Every async method has a sync counterpart; base classes provide default delegations to sync so custom subclasses work without changes. Token callbacks use `AnyTokenCallbackType` (union of sync/async) with `_invoke_token_callback()` for transparent dispatch.
- Async LLM hard timeouts belong inside the tenacity-decorated attempt so each timeout is retryable; stream idle timeouts wrap each `__anext__()` call so receiving a chunk resets the idle deadline. Both become LiteLLM `Timeout` errors before retry classification and eventually map to `LLMTimeoutError` after retries are exhausted.
- `conversation.interrupt()` cancels in-flight `arun()` by cancelling the tracked `_arun_task`. `asyncio.CancelledError` propagates through all layers (LLM HTTP stream → agent step → conversation loop) without needing per-layer interrupt APIs, because LLM and Agent are frozen/stateless Pydantic models that may be shared across conversations. `arun()` catches `CancelledError`, sets status to `PAUSED`, and emits `InterruptEvent`. The agent-server exposes this via `EventService.interrupt()` → `ConversationService.interrupt_conversation()` → `POST /{conversation_id}/interrupt`.
- OpenHands provider LLMs keep public config as `openhands/<model>` and translate to `litellm_proxy/<model>` plus the OpenHands proxy `api_base` only at LiteLLM boundaries. Legacy persisted proxy payloads are migrated once at settings/profile load boundaries; do not add downstream UI reverse-mapping for the normal path.
- Programmatic settings live in `openhands-sdk/openhands/sdk/settings/`. Treat `AgentSettings` and `export_settings_schema()` as the canonical structured settings surface in the SDK, and keep that schema focused on neutral config structure and semantics; downstream apps should own client-specific ordering, icons, widgets, and slash-command presentation.
- `ACP_PROVIDERS` declares which ACP harnesses exist, not which ones a product offers. Product clients declare `SURFACED_ACP_PROVIDERS` independently and assert only `SURFACED_ACP_PROVIDERS ⊆ ACP_PROVIDERS`; this makes a rename or removal fail while an addition is a no-op. Do not require a complementary not-surfaced list or add deployment policy to `ACPProviderInfo`.
- `SettingsFieldSchema` intentionally does not export a `required` flag. If a consumer needs nullability semantics, inspect the underlying Python typing rather than inferring from SDK defaults.
- `AgentSettings.tools` is part of the exported settings schema so the schema stays aligned with the settings payload that round-trips through `AgentSettings` and drives `create_agent()`.
- `mcp_config` is declared on `OpenHandsAgentSettings` and `ACPAgentSettings` (and inherited by `LLMAgentSettings`) — the variants of the `AgentSettingsConfig` union. It is a flat `dict[str, MCPServer]` server map, where `MCPServer` is the SDK's own model in `openhands-sdk/openhands/sdk/mcp/config.py` — not FastMCP's `MCPConfig`, and not the `.mcp.json`-style `{"mcpServers": {...}}` wrapper (the v4→v5 migration `_migrate_mcp_config_to_server_map` unwraps that key). FastMCP's typed `MCPConfig` is built only at the FastMCP boundary, in `_prepare_mcp_config()` in `openhands-sdk/openhands/sdk/mcp/utils.py`, from the payload produced by `to_fastmcp_mcp_config()` in `openhands-sdk/openhands/sdk/mcp/config.py`. Keep serialized MCP output compact with `exclude_none=True, exclude_defaults=True`, applied per-`MCPServer` in `openhands-sdk/openhands/sdk/mcp/config.py`.
- Persisted SDK settings should use the direct `model_dump()` shape with a top-level `schema_version`; avoid adding wrapped payload formats or legacy migration shims in `openhands/sdk/settings/model.py`.
- Persisted settings compatibility is enforced by `.github/scripts/check_persisted_settings_compat.py` plus `tests/sdk/persisted_settings_baselines/vN/` golden fixtures. When a versioned persisted settings shape changes incompatibly, bump the relevant schema version constant, add the migration step, and add a fixture for the old shape.
- The persisted-settings compatibility check builds its historical PyPI baseline environment with `uv` and a PyPI release-date `exclude-newer` cutoff, and golden fixtures may include a top-level `__expected__` map of dotted paths whose post-migration values must be preserved.
- Prefer removing genuinely temporary compatibility fields and serializers outright rather than carrying legacy settings shims in the SDK. Note that persisted settings now ship with a versioned migration chain plus golden fixtures and a CI gate, so check `tests/sdk/persisted_settings_baselines/` before deleting anything that looks like a shim — it may be load-bearing for an older persisted payload.
- Do not expose settings schema versions as public `CURRENT_PERSISTED_VERSION` class constants on `AgentSettings` or `ConversationSettings`; keep versioning internal to the `schema_version` field/defaults and private module constants.
- `ConversationSettings` owns the conversation-scoped confirmation controls directly (`confirmation_mode`, `security_analyzer`); keep those fields top-level on the model and grouped into the exported `verification` section via schema metadata rather than nested helper models, and prefer the direct settings-model constructor `create_request(...)` over separate request-wrapper helpers.
- Anthropic malformed tool-use/tool-result history errors (for example, missing or duplicated ``tool_result`` blocks) are intentionally mapped to a dedicated `LLMMalformedConversationHistoryError` and caught separately in `Agent.step()`, so recovery can still use condensation while logs preserve that this was malformed history rather than a true context-window overflow.
- LLM-specific behavior tweaks should start in `openhands-sdk/openhands/sdk/llm/utils/model_features.py` whenever they can be expressed as model/provider capabilities. Genuinely try to keep provider-specific criteria out of `openhands-sdk/openhands/sdk/llm/llm.py`; only touch `llm.py` when the behavior cannot be represented cleanly in the feature registry or a focused helper.
- AgentSkills progressive disclosure resolves in `AgentContext._resolve_dynamic_data()`, shared by `get_system_message_suffix()` and the prompt section registry (`openhands/sdk/context/prompts/sections/`), and lands in `<available_skills>`. `openhands.sdk.skills.to_prompt()` (`openhands-sdk/openhands/sdk/skills/skill.py`) truncates each prompt description to 1024 characters because the AgentSkills specification caps `description` at 1-1024 characters.
- Workspace-wide uv resolver guardrails belong in the repository root `[tool.uv]` table. When `exclude-newer` is configured there, `uv lock` persists it into the root `uv.lock` `[options]` section as both an absolute cutoff and `exclude-newer-span`, and `uv sync --frozen` continues to use that locked workspace state.
- PR code review is handled via OpenHands Cloud automation (not a GitHub Actions workflow). Repo-specific reviewer instructions live in `.agents/skills/custom-codereview-guide.md`. The automation triggers on `ready_for_review` (established contributors), when `all-hands-bot` is requested as a reviewer, and when the `review-this` label is added.
- Release PR reviewer guidance now requires checking the latest PR-specific `Run tests`, `Run Examples Scripts`, and `Run Integration Tests` results/comments before approval; if any are missing, stale, ambiguous, skipped, or failing, the bot should leave a COMMENT and defer to human maintainer review.
- Directory-based runnable examples under `examples/` should expose their entrypoint as `main.py`, and `tests/examples/test_examples.py` should explicitly list the example directory in `_TARGET_DIRECTORIES` so the non-recursive example workflow collects it without accidentally running helper modules.
- The duplicate-issue automation scripts should validate `owner/repo` arguments before interpolating GitHub API paths, handle per-issue auto-close failures without aborting the whole batch, and keep `app_conversation_id` paths unquoted because OpenHands conversation IDs are already canonicalized for those endpoints.
- GitHub Actions workflows pin external third-party actions to a full 40-character commit SHA, with the version tag in a trailing comment (`uses: owner/repo@<sha> # v1.2.3`); mutable tags or branches are not used for third-party actions. GitHub-authored (`actions/*`, `github/*`) and first-party (`OpenHands/*`) refs are currently exempt. Dependabot's `github-actions` ecosystem still bumps the pinned SHA (and the trailing comment), so pinning does not block security or version updates.
- `agent-server` now defaults `TMUX_TMPDIR` to a per-process directory under the system temp dir (`openhands-agent-server-<pid>`) when the environment variable is unset. This isolates tmux sockets/cleanup across concurrent server instances while still respecting an explicit `TMUX_TMPDIR` override.
- Conversation worktrees for git-backed local workspaces live under `/tmp/conversation-worktrees/<conversation_id>/<repo_root.name>`, and if the original workspace points at a subdirectory inside the repo, the active workspace should preserve that relative path inside the worktree.
- Agent-server Docker publish tags are defined centrally in `openhands-agent-server/openhands/agent_server/docker/build.py`; keep `server.yml` manifest publication derived from the emitted per-arch tags so SHA/branch/git-tag aliases stay in sync, while preserving the legacy `latest-<variant>` alias used by workspace defaults.
- The published agent-server Docker images in `.github/workflows/server.yml` must pass `OPENHANDS_BUILD_GIT_SHA` and `OPENHANDS_BUILD_GIT_REF` as explicit `docker/build-push-action` build args; the workflow only uses `docker/build.py` for context/tag generation, so those runtime env vars are otherwise left at the Dockerfile `unknown` defaults.
- The PyInstaller agent-server binary should copy OpenHands distribution metadata (`openhands-agent-server`, `openhands-sdk`, `openhands-tools`, `openhands-workspace`) in `openhands-agent-server/openhands/agent_server/agent-server.spec`, otherwise `/server_info` version lookups via `importlib.metadata` can fall back to `unknown` inside published binary images.
- Agent-server deferred init (warm-pool / dormant mode) is driven by `Config.deferred_init` (env `OH_DEFERRED_INIT`). The `InitService` in `openhands-agent-server/openhands/agent_server/init_router.py` owns the dormant→initializing→ready transition and is registered on `app.state.init_service` only when `deferred_init=True`; the `require_initialized` dependency, added to the `/api/*` router, returns 503 while not `ready`. Bootstrap auth for `POST /api/init` uses the existing `secret_key` (`X-Init-API-Key` header) — the orchestrator already holds this key for encryption, and it is overwritten when the per-user runtime config arrives in the init body. The agent-server's 5xx exception handler rewrites `detail` on 503s, so warm-pool orchestrators should rely on the HTTP status code (not the body) when probing dormant state.
- Auto-title generation should not re-read `ConversationState.events` from a background task triggered by a freshly received `MessageEvent`; extract message text synchronously from the incoming event and then reuse shared title helpers (`extract_message_text`, `generate_title_from_message`) to avoid persistence-order races.
- `RemoteConversation.generate_title()` now reconciles remote events and reuses the shared local `generate_conversation_title(...)` helper instead of calling the removed deprecated agent-server `/generate_title` REST route, so explicit remote title generation still works without a transport-only compatibility endpoint.
- Remote workspace git operations should call `/api/git/changes` and `/api/git/diff` via the `path` query parameter with slash-normalized strings; building those URLs with `pathlib.Path` leaks host-platform separators and breaks Windows paths. The grep tool now prefers `rg`, then system `grep`, then Python; both the real grep executor and the SDK's terminal-command compatibility fallback should keep that order. For grep parity, the Python fallback should hide dotfiles by default but still let explicit `include` globs surface files like `.env`, matching ripgrep. For glob parity, any symlink-preservation regression test should force the Python fallback path, because ripgrep availability changes whether the fallback implementation runs at all.
- Keep path helpers split by purpose: `is_absolute_path_source()` is for cross-platform source/wire syntax detection, while local filesystem writes/validation (for example, the file editor) should use host-native absolute-path semantics so POSIX does not silently accept Windows drive paths as creatable files.
- Tool availability filtering belongs in `openhands-sdk/openhands/sdk/tool/registry.py` via `list_usable_tools()`, which preserves registration order and defaults tools to usable unless they expose an `is_usable()` callable. Environment-specific checks like Chromium detection should live on the concrete tool class (`BrowserToolSet.is_usable()`), while agent-server surfaces such as `/server_info` should consume the registry helper rather than re-implement per-tool filtering.
- Pydantic secret field helpers live in `openhands-sdk/openhands/sdk/utils/pydantic_secrets.py`. `serialize_secret()` handles serialization (cipher / `expose_secrets` / default Pydantic masking); `validate_secret()` handles deserialization (cipher decryption, redacted/empty → `None`); `is_redacted_secret()` checks for the sentinel; `REDACTED_SECRET_VALUE` is the canonical sentinel string. For `dict[str, str]` fields whose values are all secrets, wrap each value in `SecretStr` and call `serialize_secret` per value (see `LookupSecret._serialize_secrets`). Do not hand-roll redaction logic in field serializers.
- Agent-server persistence detects whether settings or secret payloads contain values by exercising the real Pydantic secret-serialization pipeline with `_SecretProbeCipher` (`persistence/models.py`). Do not replace this with a hardcoded field list or a recursive `SecretStr` walk: fields such as `AgentContext.secrets` store plain strings and only become secret-bearing inside their serializers.
- `LookupSecret` normalizes hostless URLs against `OH_INTERNAL_SERVER_URL` (set by `openhands-agent-server.__main__` from the bound host/port, rewriting wildcard binds to loopback) and otherwise falls back to `http://127.0.0.1:8000`, so relative secret URLs can safely target the current agent-server instance.
## Package-specific guidance
When reviewing or modifying code, read the closest AGENTS file for the
package(s) containing the changed files. If a PR spans multiple packages,
consult each relevant package-level AGENTS.md.
- SDK: [openhands-sdk/openhands/sdk/AGENTS.md](openhands-sdk/openhands/sdk/AGENTS.md)
- Subagents: [openhands-sdk/openhands/sdk/subagent/AGENTS.md](openhands-sdk/openhands/sdk/subagent/AGENTS.md)
- Tools: [openhands-tools/openhands/tools/AGENTS.md](openhands-tools/openhands/tools/AGENTS.md)
- Workspace: [openhands-workspace/openhands/workspace/AGENTS.md](openhands-workspace/openhands/workspace/AGENTS.md)
- Agent server: [openhands-agent-server/AGENTS.md](openhands-agent-server/AGENTS.md)
## API compatibility pointers
- For SDK Python API deprecation/removal policy, read
[openhands-sdk/openhands/sdk/AGENTS.md](openhands-sdk/openhands/sdk/AGENTS.md).
Public API removals require deprecation metadata with a removal target at
least **5 minor releases** after `deprecated_in`, and breaking SDK API
changes require at least a **MINOR** SemVer bump.
- The SDK API breakage checker should treat metadata-only changes to
Pydantic `Field(...)` declarations as non-breaking, including adding,
removing, or editing `description`, `title`, `examples`,
`json_schema_extra`, and `deprecated` kwargs.
- Public SDK `Field(default=...)` changes are treated separately from removals/structural API breakages: the API breakage workflow should surface them as behavioral compatibility changes, auto-apply the green `release-note-required` label on PRs, and the release workflow should prepend those labeled PRs to generated GitHub release notes.
- For public SDK `Field(default=...)` changes, keep two views in the API breakage workflow: compare against the latest released PyPI baseline for compatibility reporting, but compare against the PR base ref before syncing the `release-note-required` label or PR comment so unrelated follow-up PRs are not re-labeled for already-merged unreleased defaults.
- The SDK API breakage checker compares stringified `Field(...)` values by
parsing them as Python expressions after escaping literal newlines inside
quoted strings; this avoids false positives on multiline descriptions that
include embedded quotes like `'security_policy.j2'`.
- For public REST APIs, read
[openhands-agent-server/AGENTS.md](openhands-agent-server/AGENTS.md).
REST contract breaks need a deprecation notice and a runway of
**5 minor releases** before removing the old contract or making an
incompatible replacement mandatory.
<DEV_SETUP>Signals
- Stars
- 1,300
- Forks
- 696
- Open issues
- 327
- Last commit (this folder)
- Oct 16, 2026 (13 days ago)
- Last release
- Oct 23, 2026 (6 days ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/OpenHands/software-agent-sdk
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from OpenHands
- 0.721score
release-notes
OpenHands
Generate consistent, well-structured release notes from git history.
SkillAFreshness: A★ 173Claude Code +9 - 0.721score
add-javadoc
OpenHands
Add comprehensive JavaDoc documentation to Java classes and methods.
SkillAFreshness: A★ 173Claude Code +9 - 0.721score
agent-readiness-report
OpenHands
Evaluate how well a codebase supports autonomous AI-assisted development.
SkillAFreshness: A★ 173Claude Code +9 - 0.721score
openhands
OpenHands
Unified OpenHands plugin — bundles the OpenHands Cloud CLI, Cloud REST API (openhands-api), Automations (openhands-automation), and Software Agent SDK…
SkillAFreshness: A★ 173Claude Code +9 - 0.721score
runtime-api-configs
OpenHands
Manage warm sandbox pools on OpenHands Enterprise by driving the runtime-api admin endpoints (list, save, delete warm runtime configurations) from a Python CLI.
SkillAFreshness: A★ 173Claude Code +9 - 0.721score
add-skill
OpenHands
Import an existing skill from a GitHub repository URL into the current workspace.
SkillAFreshness: A★ 173Claude Code +9
Cite this page
AGENTS.md · OpenHands/software-agent-sdk. Agent Mart. https://agentmart.dev/templates/openhands-software-agent-sdk-agents-md (last updated 2026-10-24)