Skip to content

Commit f5e2531

Browse files
examples: standardize directory entrypoints (#3259)
Co-authored-by: openhands <openhands@all-hands.dev>
1 parent 5a31572 commit f5e2531

7 files changed

Lines changed: 25 additions & 5 deletions

File tree

.agents/skills/custom-codereview-guide.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ If the updated package was uploaded **within the last 7 days**, treat it as a re
124124
- **Breaking Changes**: API changes affecting users, removed public fields/methods, changed defaults
125125
- **Code Quality**: Code duplication, missing comments for non-obvious decisions, inline imports (unless necessary for circular deps)
126126
- **Repository Conventions**: Use `pyright` not `mypy`, put fixtures in `conftest.py`, avoid `sys.path.insert` hacks
127+
- **Directory Example Entrypoints**: PRs that add or modify folder-based runnable examples under `examples/` should use `main.py` as the entrypoint and add the directory to `_TARGET_DIRECTORIES` in `tests/examples/test_examples.py`; see [Directory-Based Examples](#directory-based-examples)
127128
- **Event Type Deprecation**: Changes to event types (Pydantic models used in serialization) must handle deprecated fields properly
128129
- **Thread Safety**: New methods in `LocalConversation` that read or write `self._state` must use `with self._state:` — see the [Concurrency](#concurrency---localconversation-state-lock) section below
129130
- **Persistence Paths**: Code that computes persistence directories must not double-append the conversation hex — see the [Persistence Paths](#persistence-path-construction) section below
@@ -132,6 +133,18 @@ If the updated package was uploaded **within the last 7 days**, treat it as a re
132133
- **Secret Serialization**: Fields that carry secrets must use `serialize_secret()` from `openhands.sdk.utils.pydantic_secrets`. For `dict[str, str]` secret fields, wrap each value in `SecretStr` and call `serialize_secret` per value. Do not hand-roll redaction logic (e.g. custom sentinels or inline `expose_secrets` checks) in field serializers
133134
- **Info-Log Payloads**: `logger.info(...)` must not dump objects, dicts, or variable-length lists — see [Logging Hygiene](#logging-hygiene)
134135

136+
## Directory-Based Examples
137+
138+
When a PR adds or modifies a runnable example represented by a directory under `examples/`, verify that:
139+
140+
1. The runnable entrypoint is named `main.py`.
141+
2. Helper modules inside that directory are not accidentally treated as standalone examples.
142+
3. `tests/examples/test_examples.py` includes the example directory in `_TARGET_DIRECTORIES` when the example should run in the `test-examples` workflow.
143+
4. The example prints an `EXAMPLE_COST: ...` marker when run by the workflow.
144+
145+
Do not ask for this convention on support scripts that are intentionally named for GitHub workflow consumption (for example reusable automation scripts under `examples/03_github_workflows/`) unless they are presented as a directory-based runnable example.
146+
147+
135148
## Event Type Deprecation - Critical Review Checkpoint
136149

137150
When reviewing PRs that modify event types (e.g., `TextContent`, `Message`, `Event`, or any Pydantic model used in event serialization), **DO NOT APPROVE** until the following are verified:

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ When reviewing code, provide constructive feedback:
114114
- AgentSkills progressive disclosure goes through `AgentContext.get_system_message_suffix()` into `<available_skills>`, and `openhands.sdk.context.skills.to_prompt()` truncates each prompt description to 1024 characters because the AgentSkills specification caps `description` at 1-1024 characters.
115115
- 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.
116116
- `pr-review-by-openhands` delegates to `OpenHands/extensions/plugins/pr-review@main`. Repo-specific reviewer instructions live in `.agents/skills/custom-codereview-guide.md`, and because task-trigger matching is substring-based, that `/codereview` skill is also auto-injected for the workflow's `/codereview-roasted` prompt.
117+
- 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.
117118
- 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.
118119
- `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.
119120
- 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.

examples/01_standalone_sdk/33_hooks/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ This folder demonstrates the OpenHands hooks system.
44

55
## Example
66

7-
- **33_hooks.py** - Complete hooks demo showing all four hook types
7+
- **main.py** - Complete hooks demo showing all four hook types
88

99
## Scripts
1010

@@ -24,7 +24,7 @@ export LLM_MODEL="anthropic/claude-sonnet-4-5-20250929" # optional
2424
export LLM_BASE_URL="https://your-endpoint" # optional
2525

2626
# Run example
27-
python 33_hooks.py
27+
python main.py
2828
```
2929

3030
## Hook Types
File renamed without changes.

examples/02_remote_agent_server/06_custom_tool/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ This pattern is useful for:
4747
- **`custom_tools/log_data.py`**: Example custom tool for logging structured data to JSON
4848
- **`Dockerfile`**: Simple Dockerfile that copies custom tools into the base image
4949
- **`build_custom_image.sh`**: Script to build the custom base image
50-
- **`custom_tool_example.py`**: SDK script demonstrating the full workflow
50+
- **`main.py`**: SDK script demonstrating the full workflow
5151
- **`README.md`**: This documentation
5252

5353
## The Custom Tool
@@ -106,7 +106,7 @@ When creating a conversation, the SDK:
106106
3. Server imports those modules, triggering auto-registration
107107
4. Tools become available for agent execution
108108

109-
### 4. SDK Script (`custom_tool_example.py`)
109+
### 4. SDK Script (`main.py`)
110110

111111
The script:
112112
- Builds the custom base image (if not already built)
@@ -133,7 +133,7 @@ The script:
133133

134134
2. **Run the example**:
135135
```bash
136-
python custom_tool_example.py
136+
python main.py
137137
```
138138

139139
The script will:

examples/02_remote_agent_server/06_custom_tool/custom_tool_example.py renamed to examples/02_remote_agent_server/06_custom_tool/main.py

File renamed without changes.

tests/examples/test_examples.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@
2626
EXAMPLES_ROOT / "01_standalone_sdk",
2727
EXAMPLES_ROOT / "02_remote_agent_server",
2828
# These examples live under subdirectories (each with a single `main.py`).
29+
EXAMPLES_ROOT / "01_standalone_sdk" / "33_hooks",
2930
EXAMPLES_ROOT / "01_standalone_sdk" / "37_llm_profile_store",
3031
EXAMPLES_ROOT / "01_standalone_sdk" / "43_mixed_marketplace_skills",
32+
EXAMPLES_ROOT / "02_remote_agent_server" / "06_custom_tool",
3133
EXAMPLES_ROOT / "05_skills_and_plugins" / "01_loading_agentskills",
3234
EXAMPLES_ROOT / "05_skills_and_plugins" / "02_loading_plugins",
3335
)
@@ -88,9 +90,13 @@ def _normalize_path(path: Path) -> str:
8890

8991

9092
def test_directory_example_is_discovered() -> None:
93+
assert (EXAMPLES_ROOT / "01_standalone_sdk" / "33_hooks" / "main.py") in EXAMPLES
9194
assert (
9295
EXAMPLES_ROOT / "01_standalone_sdk" / "37_llm_profile_store" / "main.py"
9396
) in EXAMPLES
97+
assert (
98+
EXAMPLES_ROOT / "02_remote_agent_server" / "06_custom_tool" / "main.py"
99+
) in EXAMPLES
94100

95101

96102
@pytest.mark.parametrize("example_path", EXAMPLES, ids=_normalize_path)

0 commit comments

Comments
 (0)