Skip to content

Latest commit

 

History

History
345 lines (275 loc) · 32 KB

File metadata and controls

345 lines (275 loc) · 32 KB
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.

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.

  • 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.

  • 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 real migration chain (AGENT_SETTINGS_SCHEMA_VERSION is at 5) 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.

  • 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.

API compatibility pointers

  • For SDK Python API deprecation/removal policy, read 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. 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>

  • Make sure you make build to configure the dependencies first
  • We use pre-commit hooks .pre-commit-config.yaml that includes:
    • type check through pyright
    • linting and formatting with ruff (ruff-check, ruff-format) plus pycodestyle
    • yamlfmt for YAML
    • repo-specific gates: check-import-rules, check-tool-registration
  • NEVER USE mypy!
  • Do NOT commit ALL the file, just commit the relevant file you've changed!
  • In every commit message, you should add "Co-authored-by: openhands openhands@all-hands.dev"
  • You can run pytest with uv run pytest

Instruction for fixing "E501 Line too long"

  • If it is just code, you can modify it so it spans multiple lines.
  • If it is a single-line string, you can break it into a multi-line string by doing "ABC" -> ("A"\n"B"\n"C")
  • If it is a long multi-line string (e.g., docstring), add # noqa: E501 AFTER the ending """. You should NEVER ADD IT INSIDE the docstring. E501 is a ruff lint rule, so # noqa: E501 is what suppresses it — # type: ignore is a pyright directive and has no effect on E501.

</DEV_SETUP>

<PR_ARTIFACTS>

PR-Specific Evidence Documents

The .pr/ directory is intentionally temporary by repository policy: the PR Artifacts workflow (.github/workflows/pr-artifacts.yml) treats it as PR-only reviewer context and automatically removes it after PR approval.

When working on a PR that requires design documents, scripts meant for development-only, or other temporary artifacts that should NOT be merged to main, store them in a .pr/ directory at the repository root.

You can also use the .pr/ directory for evidence, such as logs, live-tests, live-test summary files.

Usage

# Create the directory if it doesn't exist
mkdir -p .pr

# Add your PR-specific documents
.pr/
├── design.md       # Design decisions and architecture notes
├── analysis.md     # Investigation or debugging notes
└── notes.md        # Any other PR-specific content

How It Works

  1. Notification: When .pr/ exists, a single comment is posted to the PR conversation alerting reviewers
  2. Auto-cleanup: When the PR is approved, the .pr/ directory is automatically removed via commit
  3. Fork PRs: Auto-cleanup cannot push to forks, so manual removal is required before merging

Important Notes

  • Do NOT put anything in .pr/ that needs to be preserved
  • The .pr/ check passes (green ✅) during development - it only posts a notification, not a blocking error
  • For fork PRs: You must manually remove .pr/ before the PR can be merged

When to Use

  • Complex refactoring that benefits from written design rationale
  • Debugging sessions where you want to document your investigation
  • Feature implementations that need temporary planning docs
  • Temporary script that are intended to show reviewers that the feature works
  • Any analysis that helps reviewers understand the PR but isn't needed long-term </PR_ARTIFACTS>

<PR_DESCRIPTION_HUMAN_CHECK>

Human-only PR description fields in PR template

When opening a PR, use the repository's PR template for the description. The HUMAN: section in PR descriptions is reserved for human contributors only. AI agents MUST NOT edit, move, or remove this field, only set the placeholder. If the PR description CI fails because this field is missing or empty, stop and ask the human user to update it in their own words. If the field was already updated by a human, report the exact validator error rather than editing it yourself. </PR_DESCRIPTION_HUMAN_CHECK>

<REVIEW_HANDLING>

  • Critically evaluate each review comment before acting on it. Not all feedback is worth implementing:
    • Does it fix a real bug or improve clarity significantly?
    • Does it align with the project's engineering principles (simplicity, maintainability)?
    • Is the suggested change proportional to the benefit, or does it add unnecessary complexity?
  • It's acceptable to respectfully decline suggestions that add verbosity without clear benefit, over-engineer for hypothetical edge cases, or contradict the project's pragmatic approach.
  • After addressing (or deciding not to address) inline review comments, mark the corresponding review threads as resolved.
  • Before resolving a thread, leave a reply comment that either explains the reason for dismissing the feedback or references the specific commit (e.g., commit SHA) that addressed the issue.
  • Prefer resolving threads only once fixes are pushed or a clear decision is documented.
  • Use the GitHub GraphQL API to reply to and resolve review threads (see below).

Resolving Review Threads via GraphQL

The CI check Review Thread Gate/unresolved-review-threads will fail if there are unresolved review threads. To resolve threads programmatically:

  1. Get the thread IDs (replace <OWNER>, <REPO>, <PR_NUMBER>):
gh api graphql -f query='
{
  repository(owner: "<OWNER>", name: "<REPO>") {
    pullRequest(number: <PR_NUMBER>) {
      reviewThreads(first: 20) {
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes { body }
          }
        }
      }
    }
  }
}'
  1. Reply to the thread explaining how the feedback was addressed:
gh api graphql -f query='
mutation {
  addPullRequestReviewThreadReply(input: {
    pullRequestReviewThreadId: "<THREAD_ID>"
    body: "Fixed in <COMMIT_SHA>"
  }) {
    comment { id }
  }
}'
  1. Resolve the thread:
gh api graphql -f query='
mutation {
  resolveReviewThread(input: {threadId: "<THREAD_ID>"}) {
    thread { isResolved }
  }
}'
  1. Get the failed workflow run ID and rerun it:
# Find the run ID from the failed check URL, or use:
gh run list --repo <OWNER>/<REPO> --branch <BRANCH> --limit 5

# Rerun failed jobs
gh run rerun <RUN_ID> --repo <OWNER>/<REPO> --failed

</REVIEW_HANDLING>

- Avoid hacky trick like `sys.path.insert` when resolving package dependency - Use existing packages/libraries instead of implementing yourselves whenever possible. - Avoid using # type: ignore. Treat it only as a last resort. In most cases, issues should be resolved by improving type annotations, adding assertions, or adjusting code/tests—rather than silencing the type checker. - Please AVOID using # type: ignore[attr-defined] unless absolutely necessary. If the issue can be addressed by adding a few extra assert statements to verify types, prefer that approach instead! - For issue like # type: ignore[call-arg]: if you discover that the argument doesn’t actually exist, do not try to mock it again in tests. Instead, simply remove it. - Avoid doing in-line imports unless absolutely necessary (e.g., circular dependency). - Avoid getattr/hasattr guards and instead enforce type correctness by relying on explicit type assertions and proper object usage, ensuring functions only receive the expected Pydantic models or typed inputs. Prefer type hints and validated models over runtime shape checks. - Prefer accessing typed attributes directly. If necessary, convert inputs up front into a canonical shape; avoid purely hypothetical fallbacks. - Use real newlines in commit messages; do not write literal "\n". - Comments policy: write comments sparingly and strategically. Prefer making the code self-explanatory through clear naming and structure over adding prose. - Do NOT add comments that restate what the code already says, summarize the surrounding diff/PR, or narrate the change history ("previously we did X, now we do Y"). That kind of context belongs in the PR description or commit message, not in the source — `git blame` and the PR are the source of truth for *why* a change was made. - Do NOT describe non-local parts of the system (other modules, callers, downstream behavior) in a comment unless there is a mechanism to keep that description in sync. Such comments drift and become misleading. - DO add a comment when the code expresses something genuinely unintuitive: a non-obvious invariant, a workaround for an external bug, a subtle ordering/locking requirement, or a deliberate trade-off the reader cannot infer from the code itself. - When in doubt, prefer restructuring or renaming over commenting. A 3-line change should not produce 19 lines of comments — if it feels like it needs that much narration, the explanation belongs in the PR description. - AFTER you edit ONE file, you should run pre-commit hook on that file via `uv run pre-commit run --files [filepath]` to make sure you didn't break it. - Don't write TOO MUCH test, you should write just enough to cover edge cases. - Check how we perform tests in .github/workflows/tests.yml - Put unit tests under the corresponding domain folder in `tests/` (e.g., `tests/sdk`, `tests/tools`, `tests/workspace`). For example, changes to `openhands-sdk/openhands/sdk/tool/tool.py` should be covered in `tests/sdk/tool/test_tool.py`. - DON'T write TEST CLASSES unless absolutely necessary! - If you find yourself duplicating logics in preparing mocks, loading data etc, these logic should be fixtures in conftest.py! - Please test only the logic implemented in the current codebase. Do not test functionality (e.g., BaseModel.model_dumps()) that is not implemented in this repository. - For changes to prompt templates, tool descriptions, or agent decision logic, add the `integration-test` label to trigger integration tests and verify no unexpected impact on benchmark performance.

Stress Tests

tests/agent_server/stress/ contains an opt-in stress/scale suite for the agent-server, excluded from default collection via the stress pytest marker. Run with uv run pytest -m stress. For full details on running, infrastructure, and adding new stress tests, see openhands-agent-server/AGENTS.md.

Behavior Tests

Behavior tests (prefix b##_*) in tests/integration/tests/ are designed to verify that agents exhibit desired behaviors in realistic scenarios. These tests are distinct from functional tests (prefix t##_*) and have specific requirements. The same directory also holds a##_* (malformed tool-use/tool-result history) and c##_* (condenser) tests; only the b/t prefixes drive the behavior-vs-integration classification described in BEHAVIOR_TESTS.md.

Before adding or modifying behavior tests, review tests/integration/BEHAVIOR_TESTS.md for the latest workflow, expectations, and examples.

<AGENT_TMP_DIRECTORY>

Agent Temporary Directory Convention

When tools need to store observation files (e.g., browser session recordings, task tracker data), use .agent_tmp as the directory name for consistency.

The browser session recording tool saves recordings to .agent_tmp/browser_observations/recording-{timestamp}/ (see BROWSER_RECORDING_OUTPUT_DIR in openhands-tools/openhands/tools/browser_use/definition.py).

This convention ensures tool-generated observation files are stored in a predictable location that can be easily:

  • Added to .gitignore
  • Cleaned up after agent sessions
  • Identified as agent-generated artifacts

Note: This is separate from persistence_dir which is used for conversation state persistence. </AGENT_TMP_DIRECTORY>

- This is a `uv`-managed Python monorepo (single `uv.lock` at repo root) with multiple distributable packages: `openhands-sdk/` (SDK), `openhands-tools/` (built-in tools), `openhands-workspace/` (workspace impls), and `openhands-agent-server/` (server runtime). - `examples/` contains runnable patterns; `tests/` is split by domain (`tests/sdk`, `tests/tools`, `tests/workspace`, `tests/agent_server`, etc.). - Python namespace is `openhands.*` across packages; keep new modules within the matching package and mirror test paths under `tests/`.

<QUICK_COMMANDS>

  • Set up the dev environment: make build (runs uv sync --dev and installs pre-commit; requires uv >= 0.8.13)
  • Lint/format: make lint, make format
  • Run tests: uv run pytest
  • Run agent-server stress tests: uv run pytest -m stress (see openhands-agent-server/AGENTS.md)
  • Build agent-server: make build-server (output: dist/agent-server/)
  • Clean caches: make clean
  • Run SDK examples: see openhands-sdk/openhands/sdk/AGENTS.md.
  • The example workflow runs uv run pytest tests/examples/test_examples.py --run-examples; each successful example must print an EXAMPLE_COST: ... line to stdout (use EXAMPLE_COST: 0 for non-LLM examples).
  • Linear walkthroughs in examples/ should use top-level code flow (e.g. with blocks, bare statements) rather than wrapping the whole example in def main(). CLI-style examples with argument-driven branches may use a main() entrypoint.
  • Conversation plugins passed via plugins=[...] are lazy-loaded on the first send_message() or run(), so example code should inspect plugin-added skills or resolved_plugins only after that first interaction. </QUICK_COMMANDS>

<REPO_CONFIG_NOTES>

  • Ruff: line-length = 88, target-version = "py313" (see pyproject.toml).
  • Ruff ignores ARG (unused arguments) under tests/**/*.py to allow pytest fixtures.
  • Repository guidance lives in the project root AGENTS.md (loaded as a third-party skill file). </REPO_CONFIG_NOTES>

<KNOWN_RACES_AND_GOTCHAS>

  • RemoteConversation._wait_for_run_completion and stop hooks: Per-field WebSocket FINISHED status events are hints, not authoritative termination. The server-side LocalConversation.run loop releases its state lock at the end of each iteration, so a FINISHED status set by agent.step() is visible to clients before the next loop iteration runs stop hooks (hook_processor.run_stop). If a stop hook returns rc=2 (denying the stop), status flips back to RUNNING and the agent gets another iteration. The client's _wait_for_run_completion therefore must not return on the first WS-delivered FINISHED. Instead, post-run full-state WebSocket snapshots are authoritative; if that snapshot is missing, the time-based hard-fallback path (TERMINAL_HARD_FALLBACK_SECS = 30.0) accepts REST-confirmed terminal status after 30 continuous seconds. ERROR/STUCK still raise immediately through _handle_conversation_status. Empirically this caused agents to consume just 0–1 iterations after a hook block on programbench retry-16; fix shipped in feat/programbench.
  • Hook events vs state.events: HookExecutionEvent is emitted via hook_processor.original_callback (the chained _on_event), so it should land in state.events when the run is allowed to complete. But because the WS-FINISHED race above used to make the client snapshot list(conversation.state.events) before the server-side hook eval ran, output.jsonl history could miss hook events while on-disk persisted events under /workspace/conversations/.../events/ had them — useful as a forensic signal that the race fired. </KNOWN_RACES_AND_GOTCHAS>