- 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.
- 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.
- Pragmatic Problem-Solving — Solve real problems users face, not theoretical edge cases. Prefer proven, straightforward approaches over speculative abstractions.
- 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.
-
Async LLM completions propagate through the full call chain:
LLM.acompletion()/LLM.aresponses()→_atransport_call()(litellmacompletion/aresponses) →RetryMixin.retry_decorator()(tenacityretry, which wraps coroutines natively — there is no separate async retry path) → condenseracondense()→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 useAnyTokenCallbackType(union of sync/async) with_invoke_token_callback()for transparent dispatch. -
conversation.interrupt()cancels in-flightarun()by cancelling the tracked_arun_task.asyncio.CancelledErrorpropagates 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()catchesCancelledError, sets status toPAUSED, and emitsInterruptEvent. The agent-server exposes this viaEventService.interrupt()→ConversationService.interrupt_conversation()→POST /{conversation_id}/interrupt. -
OpenHands provider LLMs keep public config as
openhands/<model>and translate tolitellm_proxy/<model>plus the OpenHands proxyapi_baseonly 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/. TreatAgentSettingsandexport_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. -
SettingsFieldSchemaintentionally does not export arequiredflag. If a consumer needs nullability semantics, inspect the underlying Python typing rather than inferring from SDK defaults. -
AgentSettings.toolsis part of the exported settings schema so the schema stays aligned with the settings payload that round-trips throughAgentSettingsand drivescreate_agent(). -
mcp_configis declared onOpenHandsAgentSettingsandACPAgentSettings(and inherited byLLMAgentSettings) — the variants of theAgentSettingsConfigunion. It is a flatdict[str, MCPServer]server map, whereMCPServeris the SDK's own model inopenhands-sdk/openhands/sdk/mcp/config.py— not FastMCP'sMCPConfig, and not the.mcp.json-style{"mcpServers": {...}}wrapper (the v4→v5 migration_migrate_mcp_config_to_server_mapunwraps that key). FastMCP's typedMCPConfigis built only at the FastMCP boundary, in_prepare_mcp_config()inopenhands-sdk/openhands/sdk/mcp/utils.py, from the payload produced byto_fastmcp_mcp_config()inopenhands-sdk/openhands/sdk/mcp/config.py. Keep serialized MCP output compact withexclude_none=True, exclude_defaults=True, applied per-MCPServerinopenhands-sdk/openhands/sdk/mcp/config.py. -
Persisted SDK settings should use the direct
model_dump()shape with a top-levelschema_version; avoid adding wrapped payload formats or legacy migration shims inopenhands/sdk/settings/model.py. -
Persisted settings compatibility is enforced by
.github/scripts/check_persisted_settings_compat.pyplustests/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
uvand a PyPI release-dateexclude-newercutoff, 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_VERSIONis at 5) plus golden fixtures and a CI gate, so checktests/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_VERSIONclass constants onAgentSettingsorConversationSettings; keep versioning internal to theschema_versionfield/defaults and private module constants. -
ConversationSettingsowns the conversation-scoped confirmation controls directly (confirmation_mode,security_analyzer); keep those fields top-level on the model and grouped into the exportedverificationsection via schema metadata rather than nested helper models, and prefer the direct settings-model constructorcreate_request(...)over separate request-wrapper helpers. -
Anthropic malformed tool-use/tool-result history errors (for example, missing or duplicated
tool_resultblocks) are intentionally mapped to a dedicatedLLMMalformedConversationHistoryErrorand caught separately inAgent.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.pywhenever they can be expressed as model/provider capabilities. Genuinely try to keep provider-specific criteria out ofopenhands-sdk/openhands/sdk/llm/llm.py; only touchllm.pywhen the behavior cannot be represented cleanly in the feature registry or a focused helper. -
AgentSkills progressive disclosure resolves in
AgentContext._resolve_dynamic_data(), shared byget_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 capsdescriptionat 1-1024 characters. -
Workspace-wide uv resolver guardrails belong in the repository root
[tool.uv]table. Whenexclude-neweris configured there,uv lockpersists it into the rootuv.lock[options]section as both an absolute cutoff andexclude-newer-span, anduv sync --frozencontinues 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 onready_for_review(established contributors), whenall-hands-botis requested as a reviewer, and when thereview-thislabel is added. -
Release PR reviewer guidance now requires checking the latest PR-specific
Run tests,Run Examples Scripts, andRun Integration Testsresults/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 asmain.py, andtests/examples/test_examples.pyshould explicitly list the example directory in_TARGET_DIRECTORIESso the non-recursive example workflow collects it without accidentally running helper modules. -
The duplicate-issue automation scripts should validate
owner/repoarguments before interpolating GitHub API paths, handle per-issue auto-close failures without aborting the whole batch, and keepapp_conversation_idpaths 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'sgithub-actionsecosystem still bumps the pinned SHA (and the trailing comment), so pinning does not block security or version updates. -
agent-servernow defaultsTMUX_TMPDIRto 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 explicitTMUX_TMPDIRoverride. -
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; keepserver.ymlmanifest publication derived from the emitted per-arch tags so SHA/branch/git-tag aliases stay in sync, while preserving the legacylatest-<variant>alias used by workspace defaults. -
The published agent-server Docker images in
.github/workflows/server.ymlmust passOPENHANDS_BUILD_GIT_SHAandOPENHANDS_BUILD_GIT_REFas explicitdocker/build-push-actionbuild args; the workflow only usesdocker/build.pyfor context/tag generation, so those runtime env vars are otherwise left at the Dockerfileunknowndefaults. -
The PyInstaller agent-server binary should copy OpenHands distribution metadata (
openhands-agent-server,openhands-sdk,openhands-tools,openhands-workspace) inopenhands-agent-server/openhands/agent_server/agent-server.spec, otherwise/server_infoversion lookups viaimportlib.metadatacan fall back tounknowninside published binary images. -
Agent-server deferred init (warm-pool / dormant mode) is driven by
Config.deferred_init(envOH_DEFERRED_INIT). TheInitServiceinopenhands-agent-server/openhands/agent_server/init_router.pyowns the dormant→initializing→ready transition and is registered onapp.state.init_serviceonly whendeferred_init=True; therequire_initializeddependency, added to the/api/*router, returns 503 while notready. Bootstrap auth forPOST /api/inituses the existingsecret_key(X-Init-API-Keyheader) — 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 rewritesdetailon 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.eventsfrom a background task triggered by a freshly receivedMessageEvent; 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 localgenerate_conversation_title(...)helper instead of calling the removed deprecated agent-server/generate_titleREST route, so explicit remote title generation still works without a transport-only compatibility endpoint. -
Remote workspace git operations should call
/api/git/changesand/api/git/diffvia thepathquery parameter with slash-normalized strings; building those URLs withpathlib.Pathleaks host-platform separators and breaks Windows paths. The grep tool now prefersrg, then systemgrep, 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 explicitincludeglobs 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.pyvialist_usable_tools(), which preserves registration order and defaults tools to usable unless they expose anis_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_infoshould 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_VALUEis the canonical sentinel string. Fordict[str, str]fields whose values are all secrets, wrap each value inSecretStrand callserialize_secretper value (seeLookupSecret._serialize_secrets). Do not hand-roll redaction logic in field serializers. -
LookupSecretnormalizes hostless URLs againstOH_INTERNAL_SERVER_URL(set byopenhands-agent-server.__main__from the bound host/port, rewriting wildcard binds to loopback) and otherwise falls back tohttp://127.0.0.1:8000, so relative secret URLs can safely target the current agent-server instance.
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
- Subagents: openhands-sdk/openhands/sdk/subagent/AGENTS.md
- Tools: openhands-tools/openhands/tools/AGENTS.md
- Workspace: openhands-workspace/openhands/workspace/AGENTS.md
- Agent server: openhands-agent-server/AGENTS.md
-
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 editingdescription,title,examples,json_schema_extra, anddeprecatedkwargs. -
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 greenrelease-note-requiredlabel 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 therelease-note-requiredlabel 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 buildto configure the dependencies first - We use pre-commit hooks
.pre-commit-config.yamlthat includes:- type check through pyright
- linting and formatting with ruff (
ruff-check,ruff-format) pluspycodestyle yamlfmtfor 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
- 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: E501AFTER the ending """. You should NEVER ADD IT INSIDE the docstring. E501 is a ruff lint rule, so# noqa: E501is what suppresses it —# type: ignoreis a pyright directive and has no effect on E501.
</DEV_SETUP>
<PR_ARTIFACTS>
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.
# 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- Notification: When
.pr/exists, a single comment is posted to the PR conversation alerting reviewers - Auto-cleanup: When the PR is approved, the
.pr/directory is automatically removed via commit - Fork PRs: Auto-cleanup cannot push to forks, so manual removal is required before merging
- 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
- 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>
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).
The CI check Review Thread Gate/unresolved-review-threads will fail if there are unresolved review threads. To resolve threads programmatically:
- 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 }
}
}
}
}
}
}'- 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 }
}
}'- Resolve the thread:
gh api graphql -f query='
mutation {
resolveReviewThread(input: {threadId: "<THREAD_ID>"}) {
thread { isResolved }
}
}'- 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.
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 (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>
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>
<QUICK_COMMANDS>
- Set up the dev environment:
make build(runsuv sync --devand 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 anEXAMPLE_COST: ...line to stdout (useEXAMPLE_COST: 0for non-LLM examples). - Linear walkthroughs in
examples/should use top-level code flow (e.g.withblocks, bare statements) rather than wrapping the whole example indef main(). CLI-style examples with argument-driven branches may use amain()entrypoint. - Conversation plugins passed via
plugins=[...]are lazy-loaded on the firstsend_message()orrun(), so example code should inspect plugin-added skills orresolved_pluginsonly after that first interaction. </QUICK_COMMANDS>
<REPO_CONFIG_NOTES>
- Ruff:
line-length = 88,target-version = "py313"(seepyproject.toml). - Ruff ignores
ARG(unused arguments) undertests/**/*.pyto 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_completionand stop hooks: Per-field WebSocketFINISHEDstatus events are hints, not authoritative termination. The server-sideLocalConversation.runloop releases its state lock at the end of each iteration, so aFINISHEDstatus set byagent.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_completiontherefore 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 infeat/programbench.- Hook events vs
state.events:HookExecutionEventis emitted viahook_processor.original_callback(the chained_on_event), so it should land instate.eventswhen the run is allowed to complete. But because the WS-FINISHED race above used to make the client snapshotlist(conversation.state.events)before the server-side hook eval ran,output.jsonlhistory 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>