@openhands/extensions 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.4.0"
2
+ ".": "0.4.1"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openhands/extensions",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Public OpenHands extension catalogs for skills, plugins, integrations, and automation templates.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "extensions"
3
- version = "0.4.0"
3
+ version = "0.4.1"
4
4
  description = "OpenHands extensions, plugins, and skills"
5
5
  requires-python = ">=3.12"
6
6
 
@@ -11,79 +11,222 @@ triggers:
11
11
 
12
12
  # GitHub PR Reviewer Automation
13
13
 
14
- Set up a cron or event-triggered automation that reviews GitHub pull requests.
14
+ Create a cron automation that polls a GitHub repository, reviews each open
15
+ pull request exactly once, and posts the AI review as a GitHub comment.
16
+
17
+ The automation script is fully deterministic: PR discovery, state tracking,
18
+ and deduplication are handled in Python. The LLM is only invoked to write
19
+ the review text for PRs not yet seen, never for orchestration.
15
20
 
16
21
  ---
17
22
 
18
23
  ## Prerequisites
19
24
 
20
- ### Required integration
25
+ ### Required secret
21
26
 
22
- - **GitHub MCP** must be installed in Settings → MCP.
27
+ Verify that the following secret is set in **OpenHands Settings → Secrets**:
23
28
 
24
- ### Information to collect
29
+ | Secret name | Token type | Minimum permissions |
30
+ |---|---|---|
31
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private) or `public_repo` (public) |
32
+ | `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Pull requests: Read and Write |
25
33
 
26
- Ask the user for:
34
+ Check with:
35
+ ```bash
36
+ curl -s https://api.github.com/user \
37
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
38
+ | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))"
39
+ ```
27
40
 
28
- 1. **Repositories** — which repos should be watched (e.g. `myorg/backend`, `myorg/frontend`)
29
- 2. **Trigger type** — event-based (reacts to `pull_request.opened` / `pull_request.synchronize`) or cron-based polling
30
- 3. **Review tone** — thorough, concise, or friendly
31
- 4. **Auto-post or draft** — should reviews be posted as GitHub comments automatically, or saved as draft artifacts for human approval first?
32
- 5. **Filters** — any label, branch, or author filters (e.g. skip drafts, only review PRs with `needs-review` label)
41
+ If the token is missing or invalid, inform the user and stop.
33
42
 
34
43
  ---
35
44
 
36
45
  ## Setup Workflow
37
46
 
38
- ### Step 1 — Verify GitHub MCP access
47
+ Follow these steps in order.
48
+
49
+ ### Step 1 — Verify GITHUB_PERSONAL_ACCESS_TOKEN
50
+
51
+ Run the `curl` check above.
52
+
53
+ - If absent: *"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in
54
+ OpenHands Settings → Secrets."* Stop.
55
+ - If the API returns `{"message": "Bad credentials"}`: tell the user the
56
+ token is invalid and ask them to update it. Stop.
39
57
 
40
- Confirm the GitHub MCP integration is working:
58
+ ### Step 2 — Collect repository
59
+
60
+ Ask: *"Which GitHub repository should be monitored?
61
+ (Format: `owner/repo`, e.g. `myorg/backend`)"*
62
+
63
+ Validate access:
64
+ ```bash
65
+ curl -s "https://api.github.com/repos/{owner}/{repo}" \
66
+ -H "Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN" \
67
+ | python3 -c "
68
+ import json, sys
69
+ d = json.load(sys.stdin)
70
+ if 'message' in d:
71
+ print('ERROR:', d['message'])
72
+ else:
73
+ print(f\"Accessible. Private: {d.get('private')}. Permissions: {d.get('permissions')}\")
74
+ "
41
75
  ```
42
- Use the GitHub MCP to list recent pull requests in one of the target repositories.
76
+
77
+ Record `REPO = "{owner}/{repo}"`.
78
+
79
+ ### Step 3 — Collect review tone
80
+
81
+ Ask: *"What review tone should the reviewer use?
82
+ 1. Thorough (default) — comprehensive coverage of correctness, security, tests, style
83
+ 2. Concise — high-signal only, skips minor style feedback
84
+ 3. Friendly — constructive and encouraging
85
+ (Press Enter for Thorough, or type your choice or any custom style description)"*
86
+
87
+ Map the choice to `REVIEW_TONE`:
88
+
89
+ | Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |
90
+ |--------|--------------|------------------------------|
91
+ | 1 / Enter | `"thorough"` | `""` |
92
+ | 2 | `"concise"` | `""` |
93
+ | 3 | `"friendly"` | `""` |
94
+ | Custom text (e.g. "hostile pirate") | `"thorough"` | the custom text verbatim |
95
+
96
+ ### Step 4 — Collect cron schedule
97
+
98
+ Ask: *"How often should the automation poll for new PRs?
99
+ (Press Enter for the default: every 5 minutes.
100
+ Use a cron expression for a different interval, e.g. `0 * * * *` = hourly)"*
101
+
102
+ Default: `*/5 * * * *`.
103
+
104
+ Record as `CRON_SCHEDULE`.
105
+
106
+ ### Step 5 — Generate the automation script
107
+
108
+ Read `scripts/main.py` from this skill's directory. Apply exactly four
109
+ constant substitutions near the top of the file:
110
+
111
+ | Placeholder | Replace with |
112
+ |---|---|
113
+ | `REPO = "owner/repo"` | `REPO = "{owner_repo}"` |
114
+ | `REVIEW_TONE = "thorough"` | `REVIEW_TONE = "{review_tone}"` |
115
+ | `REVIEW_STYLE_INSTRUCTIONS = ""` | `REVIEW_STYLE_INSTRUCTIONS = "{style_instructions}"` |
116
+ | `DEFAULT_OPENHANDS_URL = "http://localhost:8000"` | leave unchanged unless the user has a preference |
117
+
118
+ Write the customised script to a temporary build directory:
119
+ ```bash
120
+ mkdir -p /tmp/pr-reviewer-build
121
+ # write the customised main.py to /tmp/pr-reviewer-build/main.py
43
122
  ```
44
123
 
45
- If it fails, tell the user to install the GitHub MCP integration first.
124
+ Validate syntax before packaging:
125
+ ```bash
126
+ python3 -m py_compile /tmp/pr-reviewer-build/main.py && echo "Syntax OK"
127
+ ```
46
128
 
47
- ### Step 2 — Determine trigger type
129
+ Fix any syntax errors before proceeding.
48
130
 
49
- **Event-based (recommended if publicly reachable):**
50
- Check `<RUNTIME_SERVICES>` for deployment reachability. If the deployment has a public URL, recommend an event trigger on `pull_request.opened` and `pull_request.synchronize`.
131
+ ### Step 6 — Package and upload
51
132
 
52
- **Cron-based (local/private deployments):**
53
- If the deployment is local, set up a cron schedule (e.g. every 5 minutes) that polls for recently updated PRs.
133
+ Determine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`
134
+ block in your system context:
135
+ - **OPENHANDS_HOST**: the Automation backend `url_from_agent`
136
+ - **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
54
137
 
55
- ### Step 3 — Build the review prompt
138
+ ```bash
139
+ tar -czf /tmp/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .
56
140
 
57
- Construct a prompt for the automation that includes:
58
- - The user's chosen repositories
59
- - Review focus areas (correctness, security, tests, maintainability)
60
- - Review tone preference
61
- - Whether to auto-post or draft
141
+ TARBALL_PATH=$(curl -s -X POST \
142
+ "${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer" \
143
+ -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
144
+ -H "Content-Type: application/gzip" \
145
+ --data-binary @/tmp/pr-reviewer.tar.gz \
146
+ | python3 -c "import json,sys; print(json.load(sys.stdin)['tarball_path'])")
62
147
 
63
- ### Step 4 — Create the automation
148
+ echo "Uploaded: $TARBALL_PATH"
149
+ ```
64
150
 
65
- Read the Automation backend URL and auth from `<RUNTIME_SERVICES>`:
66
- - Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`
67
- - Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`
151
+ ### Step 7 — Register the automation
68
152
 
69
- Use the **prompt preset** endpoint:
70
153
  ```bash
71
- curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1/preset/prompt" \
154
+ curl -s -X POST "${OPENHANDS_HOST}/api/automation/v1" \
72
155
  -H "X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY" \
73
156
  -H "Content-Type: application/json" \
74
- -d '{
75
- "name": "GitHub PR Reviewer",
76
- "prompt": "<constructed review prompt>",
77
- "trigger": <trigger config from step 2>
78
- }'
157
+ -d "{
158
+ \"name\": \"GitHub PR Reviewer: {owner}/{repo}\",
159
+ \"trigger\": {\"type\": \"cron\", \"schedule\": \"{cron_schedule}\"},
160
+ \"tarball_path\": \"$TARBALL_PATH\",
161
+ \"entrypoint\": \"python3 main.py\",
162
+ \"timeout\": 300
163
+ }" | python3 -m json.tool
79
164
  ```
80
165
 
81
- ### Step 5 — Confirm
166
+ Record the returned `id`.
167
+
168
+ ### Step 8 — Confirm
82
169
 
83
170
  Tell the user:
171
+
84
172
  > ✅ **GitHub PR Reviewer** is running!
85
173
  >
86
174
  > - Automation ID: `{id}`
87
- > - Repositories: `{repo list}`
88
- > - Trigger: `{trigger description}`
89
- > - Review style: `{tone}`, `{auto-post or draft}`
175
+ > - Repository: `{owner}/{repo}`
176
+ > - Review tone: `{tone}`
177
+ > - Polling schedule: `{cron_schedule}`
178
+ > - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_{id}.json`
179
+ >
180
+ > The next cron run will discover all currently open PRs and queue reviews.
181
+ > Each PR is reviewed exactly once; state is stored in the JSON file above.
182
+ > To force a re-review of all PRs, delete the state file.
183
+
184
+ ---
185
+
186
+ ## Runtime Behaviour (per poll)
187
+
188
+ Each cron run executes `main.py`, which:
189
+
190
+ 1. **Loads state** from the JSON file (see `references/state-schema.md`).
191
+ 2. **Resolves and validates `GITHUB_PERSONAL_ACCESS_TOKEN`** — aborts
192
+ immediately if absent or invalid.
193
+ 3. **Lists all open PRs** in the configured repository.
194
+ 4. **For each PR not yet in state**:
195
+ - Fetches the unified diff via the GitHub API.
196
+ - Skips PRs whose diff exceeds `MAX_DIFF_LINES_SKIP` (default 5000 lines)
197
+ and posts an explanatory comment.
198
+ - Truncates diffs larger than `MAX_DIFF_LINES` (default 500 lines) and
199
+ notes this in the prompt.
200
+ - Creates an OpenHands conversation with the PR metadata, diff, and
201
+ configured tone instructions as the initial message.
202
+ - Posts an acknowledgement comment on the PR with a link to the conversation.
203
+ - Records the PR in state with `status: "active"`.
204
+ 5. **For each active conversation**:
205
+ - Skips PRs that have been closed or merged (marks them closed silently).
206
+ - Checks the conversation's `execution_status`.
207
+ - When it reaches `idle`, `finished`, `error`, or `stuck`:
208
+ posts the agent's final response as a GitHub comment and marks the
209
+ conversation `closed`.
210
+ 6. **Saves updated state** and fires the completion callback.
211
+
212
+ ---
213
+
214
+ ## Additional Resources
215
+
216
+ - **`references/state-schema.md`** — State JSON schema, field definitions,
217
+ and conversation lifecycle diagram.
218
+ - **`scripts/main.py`** — The complete automation script. Customise the
219
+ four constants at the top before packaging.
220
+
221
+ ---
222
+
223
+ ## Troubleshooting
224
+
225
+ | Symptom | Likely cause | Fix |
226
+ |---|---|---|
227
+ | Bot never posts reviews | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token; check Step 1 |
228
+ | "Bad credentials" in run logs | Token expired | Rotate and update the secret |
229
+ | 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |
230
+ | Same PR reviewed twice | State file deleted or corrupted | Check that the state file path is stable across runs |
231
+ | Review never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI |
232
+ | PR skipped silently | Diff too large | Raise `MAX_DIFF_LINES_SKIP` in the script or split the PR |
@@ -0,0 +1,98 @@
1
+ # State File Schema
2
+
3
+ The automation maintains a JSON state file that persists across polling runs.
4
+ It is the source of truth for which PRs have been reviewed and which
5
+ conversations are still active.
6
+
7
+ ---
8
+
9
+ ## File Location
10
+
11
+ ```
12
+ {WORKSPACE_BASE_ROOT}/automation-state/github_pr_reviewer_{automation_id}.json
13
+ ```
14
+
15
+ `WORKSPACE_BASE_ROOT` is derived by going two levels up from the `WORKSPACE_BASE`
16
+ environment variable (stripping `automation-runs/{run_id}`).
17
+
18
+ Example on a local install:
19
+
20
+ ```
21
+ ~/.openhands/workspaces/automation-state/github_pr_reviewer_abc12345-….json
22
+ ```
23
+
24
+ The `automation_id` is read from the `AUTOMATION_EVENT_PAYLOAD` environment
25
+ variable (field `automation_id`).
26
+
27
+ ---
28
+
29
+ ## Top-Level Schema
30
+
31
+ ```jsonc
32
+ {
33
+ "version": 1, // schema version (integer)
34
+ "repo": "owner/repo", // the monitored repository
35
+ "conversations": { } // see ConversationRecord below
36
+ }
37
+ ```
38
+
39
+ ---
40
+
41
+ ## `conversations` Map
42
+
43
+ Key: `"{pr_number}"` (string) — uniquely identifies a PR in the repo.
44
+
45
+ Value: **ConversationRecord**
46
+
47
+ ```jsonc
48
+ {
49
+ // Always present
50
+ "pr_number": 42, // GitHub PR number (integer)
51
+ "html_url": "https://github.com/owner/repo/pull/42", // PR URL
52
+ "status": "active", // "active" | "closed" | "skipped"
53
+ "last_activity": 1717200000.0, // Unix timestamp of last state change
54
+
55
+ // Present when status is "active" or "closed"
56
+ "conversation_id": "550e8400-e29b-41d4-a716-446655440000",
57
+ // OpenHands conversation UUID
58
+
59
+ // Present when status is "skipped"
60
+ "reason": "diff too large (6000 lines)"
61
+ }
62
+ ```
63
+
64
+ ---
65
+
66
+ ## Conversation Lifecycle
67
+
68
+ ```
69
+ PR opened on GitHub
70
+ │
71
+ ▼
72
+ [active] ── conversation created, acknowledgement comment posted
73
+ │
74
+ ▼ (on a later cron run, when conversation reaches terminal status)
75
+ [closed] ── review posted to PR as GitHub comment
76
+ │
77
+ ▼ (if PR is merged/closed before review is ready)
78
+ [closed] ── no comment posted (silently closed)
79
+
80
+ ── or ──
81
+
82
+ PR diff too large
83
+ │
84
+ ▼
85
+ [skipped] ── explanatory comment posted, PR never re-queued
86
+ ```
87
+
88
+ ---
89
+
90
+ ## Resetting State
91
+
92
+ To force a re-review of all PRs (e.g. after changing the review tone):
93
+
94
+ 1. Delete the state file:
95
+ ```bash
96
+ rm ~/.openhands/workspaces/automation-state/github_pr_reviewer_<id>.json
97
+ ```
98
+ 2. The next cron run will treat all open PRs as new and queue reviews for each.
@@ -0,0 +1,625 @@
1
+ """
2
+ GitHub PR Reviewer - OpenHands Automation Script
3
+
4
+ Polls a GitHub repository on a cron schedule. For each open pull request
5
+ that has not yet been reviewed it:
6
+ 1. Fetches the PR metadata and diff.
7
+ 2. Creates an OpenHands conversation with a targeted review prompt.
8
+ 3. When the conversation completes, posts the AI review as a GitHub comment.
9
+
10
+ On subsequent runs:
11
+ - Already-reviewed PRs are skipped (tracked in the state file).
12
+ - Active conversations are checked for completion and results posted.
13
+
14
+ Configuration constants are embedded at automation-creation time by the skill.
15
+ See SKILL.md for the full setup workflow.
16
+
17
+ Required secret (set in OpenHands Settings → Secrets):
18
+ GITHUB_PERSONAL_ACCESS_TOKEN - Personal Access Token
19
+ Classic PAT: 'repo' scope (private) or 'public_repo' (public)
20
+ Fine-grained PAT: Pull requests: Read and Write
21
+
22
+ Optional secret:
23
+ OPENHANDS_URL - base URL for conversation links (default: http://localhost:8000)
24
+ """
25
+
26
+ import json
27
+ import os
28
+ import sys
29
+ import time
30
+ import urllib.error
31
+ import urllib.request
32
+ from datetime import datetime, timezone
33
+ from pathlib import Path
34
+ from urllib.parse import urlencode
35
+
36
+ # ── Embedded configuration (filled in by the skill at creation time) ──────────
37
+ REPO = "owner/repo" # e.g. "myorg/backend"
38
+ REVIEW_TONE = "thorough" # "thorough" | "concise" | "friendly"
39
+ REVIEW_STYLE_INSTRUCTIONS = "" # extra free-form persona / style notes
40
+ DEFAULT_OPENHANDS_URL = "http://localhost:8000"
41
+
42
+ # Max diff lines to include in the review prompt (avoids token overrun).
43
+ MAX_DIFF_LINES = 500
44
+
45
+ # PRs whose diff exceeds this many lines are skipped with an explanatory comment.
46
+ MAX_DIFF_LINES_SKIP = 5000
47
+
48
+ # Prevent posting summaries in the same run that created the conversation.
49
+ DONE_DEBOUNCE = 15
50
+
51
+
52
+ # ── Stdlib helpers ─────────────────────────────────────────────────────────────
53
+
54
+ def _get_env_key() -> str:
55
+ return (
56
+ os.environ.get("SESSION_API_KEY")
57
+ or os.environ.get("OH_SESSION_API_KEYS_0")
58
+ or ""
59
+ )
60
+
61
+
62
+ def get_secret(name: str) -> str:
63
+ """Fetch a named secret from the agent server."""
64
+ url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
65
+ key = _get_env_key()
66
+ req = urllib.request.Request(
67
+ f"{url}/api/settings/secrets/{name}",
68
+ headers={"X-Session-API-Key": key},
69
+ )
70
+ with urllib.request.urlopen(req) as r:
71
+ return r.read().decode().strip()
72
+
73
+
74
+ def fire_callback(
75
+ status: str = "COMPLETED",
76
+ error: str | None = None,
77
+ conversation_id: str | None = None,
78
+ ) -> None:
79
+ """Signal run completion to the automation service."""
80
+ url = os.environ.get("AUTOMATION_CALLBACK_URL", "")
81
+ if not url:
82
+ return
83
+ body: dict = {"status": status, "run_id": os.environ.get("AUTOMATION_RUN_ID", "")}
84
+ if error:
85
+ body["error"] = error
86
+ if conversation_id:
87
+ body["conversation_id"] = conversation_id
88
+ req = urllib.request.Request(
89
+ url,
90
+ data=json.dumps(body).encode(),
91
+ headers={
92
+ "Content-Type": "application/json",
93
+ "Authorization": f"Bearer {os.environ.get('AUTOMATION_CALLBACK_API_KEY', '')}",
94
+ },
95
+ )
96
+ try:
97
+ urllib.request.urlopen(req)
98
+ except Exception as exc:
99
+ print(f"Callback error (non-fatal): {exc}")
100
+
101
+
102
+ # ── State management ───────────────────────────────────────────────────────────
103
+
104
+ def _state_file_path() -> str:
105
+ """Derive a persistent storage path from WORKSPACE_BASE.
106
+
107
+ WORKSPACE_BASE = {root}/automation-runs/{run_id}
108
+ State lives two levels up at {root}/automation-state/.
109
+ """
110
+ workspace_base = os.environ.get("WORKSPACE_BASE", "")
111
+ event_payload = json.loads(os.environ.get("AUTOMATION_EVENT_PAYLOAD", "{}"))
112
+ automation_id = event_payload.get("automation_id", "default")
113
+
114
+ if workspace_base:
115
+ root = Path(workspace_base).resolve().parent.parent
116
+ else:
117
+ root = Path.home() / ".openhands" / "workspaces"
118
+
119
+ state_dir = root / "automation-state"
120
+ state_dir.mkdir(parents=True, exist_ok=True)
121
+ return str(state_dir / f"github_pr_reviewer_{automation_id}.json")
122
+
123
+
124
+ def load_state(path: str) -> dict:
125
+ if os.path.exists(path):
126
+ try:
127
+ with open(path) as f:
128
+ return json.load(f)
129
+ except (json.JSONDecodeError, OSError) as exc:
130
+ print(f"Warning: state file {path} unreadable ({exc}); starting fresh")
131
+ return {
132
+ "version": 1,
133
+ "repo": REPO,
134
+ "conversations": {}, # pr_number (str) → ConversationRecord
135
+ }
136
+
137
+
138
+ def save_state(path: str, state: dict) -> None:
139
+ with open(path, "w") as f:
140
+ json.dump(state, f, indent=2)
141
+
142
+
143
+ # ── GitHub API helpers ─────────────────────────────────────────────────────────
144
+
145
+ def _github_request(
146
+ token: str,
147
+ method: str,
148
+ path: str,
149
+ params: dict | None = None,
150
+ body: dict | None = None,
151
+ accept: str = "application/vnd.github+json",
152
+ ) -> tuple:
153
+ """Low-level GitHub API call. Returns (parsed_body, response_headers)."""
154
+ base = "https://api.github.com"
155
+ url = f"{base}{path}"
156
+ if params:
157
+ url = f"{url}?{urlencode(params)}"
158
+ headers = {
159
+ "Authorization": f"Bearer {token}",
160
+ "Accept": accept,
161
+ "X-GitHub-Api-Version": "2022-11-28",
162
+ "Content-Type": "application/json",
163
+ }
164
+ data = json.dumps(body).encode() if body is not None else None
165
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
166
+ with urllib.request.urlopen(req) as r:
167
+ resp_headers = dict(r.headers)
168
+ raw = r.read()
169
+ if accept == "application/vnd.github.diff":
170
+ return raw.decode("utf-8", errors="replace"), resp_headers
171
+ return (json.loads(raw) if raw.strip() else {}), resp_headers
172
+
173
+
174
+ def _github_paginate(token: str, path: str, params: dict | None = None) -> list:
175
+ """Fetch all pages from a GitHub list endpoint."""
176
+ results = []
177
+ page = 1
178
+ base_params = dict(params or {})
179
+ base_params.setdefault("per_page", 100)
180
+ while True:
181
+ base_params["page"] = page
182
+ data, _ = _github_request(token, "GET", path, params=base_params)
183
+ if not isinstance(data, list):
184
+ break
185
+ results.extend(data)
186
+ if len(data) < base_params["per_page"]:
187
+ break
188
+ page += 1
189
+ return results
190
+
191
+
192
+ def _resolve_github_token() -> str:
193
+ try:
194
+ token = get_secret("GITHUB_PERSONAL_ACCESS_TOKEN")
195
+ if token:
196
+ return token
197
+ except Exception:
198
+ pass
199
+ raise RuntimeError(
200
+ "GITHUB_PERSONAL_ACCESS_TOKEN secret is not set. "
201
+ "Go to OpenHands Settings → Secrets and add your GitHub Personal Access Token."
202
+ )
203
+
204
+
205
+ def _verify_token_and_repo(token: str, repo: str) -> str:
206
+ """Verify token validity and repo access. Returns the authenticated GitHub username."""
207
+ try:
208
+ user_data, _ = _github_request(token, "GET", "/user")
209
+ except urllib.error.HTTPError as exc:
210
+ if exc.code == 401:
211
+ raise RuntimeError(
212
+ "GITHUB_PERSONAL_ACCESS_TOKEN is invalid or expired. "
213
+ "Update it in OpenHands Settings → Secrets."
214
+ )
215
+ raise RuntimeError(f"GitHub /user check failed: {exc.code}")
216
+
217
+ username: str = user_data.get("login", "?")
218
+ print(f"Authenticated as GitHub user: {username}")
219
+
220
+ try:
221
+ _github_request(token, "GET", f"/repos/{repo}")
222
+ except urllib.error.HTTPError as exc:
223
+ if exc.code == 404:
224
+ raise RuntimeError(
225
+ f"Repository '{repo}' not found or not accessible with the current token."
226
+ )
227
+ raise RuntimeError(f"GitHub /repos/{repo} check failed: {exc.code}")
228
+
229
+ print(f"Repository '{repo}' accessible.")
230
+ return username
231
+
232
+
233
+ def _list_open_prs(token: str, repo: str) -> list[dict]:
234
+ """Fetch all open pull requests, oldest first."""
235
+ return _github_paginate(
236
+ token,
237
+ f"/repos/{repo}/pulls",
238
+ {"state": "open", "sort": "created", "direction": "asc"},
239
+ )
240
+
241
+
242
+ def _get_pr_diff(token: str, repo: str, pr_number: int) -> str:
243
+ """Fetch the unified diff for a pull request."""
244
+ diff, _ = _github_request(
245
+ token, "GET", f"/repos/{repo}/pulls/{pr_number}",
246
+ accept="application/vnd.github.diff",
247
+ )
248
+ return diff
249
+
250
+
251
+ def _post_github_comment(token: str, repo: str, pr_number: int, body: str) -> None:
252
+ """Post a comment on a pull request."""
253
+ try:
254
+ _github_request(
255
+ token, "POST",
256
+ f"/repos/{repo}/issues/{pr_number}/comments",
257
+ body={"body": body},
258
+ )
259
+ except Exception as exc:
260
+ print(f" Warning: failed to post comment on PR #{pr_number}: {exc}")
261
+
262
+
263
+ # ── OpenHands conversation helpers ────────────────────────────────────────────
264
+
265
+ def _oh_request(
266
+ agent_url: str, api_key: str, method: str, path: str, body: dict | None = None
267
+ ) -> dict:
268
+ url = f"{agent_url}{path}"
269
+ headers = {"X-Session-API-Key": api_key, "Content-Type": "application/json"}
270
+ data = json.dumps(body).encode() if body is not None else None
271
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
272
+ try:
273
+ with urllib.request.urlopen(req) as r:
274
+ raw = r.read()
275
+ return json.loads(raw) if raw.strip() else {}
276
+ except urllib.error.HTTPError as exc:
277
+ body_text = exc.read().decode()
278
+ raise RuntimeError(f"Agent API {method} {path} → {exc.code}: {body_text}") from exc
279
+
280
+
281
+ def _fetch_settings(agent_url: str, api_key: str) -> dict:
282
+ url = f"{agent_url}/api/settings"
283
+ headers = {"X-Session-API-Key": api_key, "X-Expose-Secrets": "plaintext"}
284
+ req = urllib.request.Request(url, headers=headers)
285
+ with urllib.request.urlopen(req) as r:
286
+ return json.loads(r.read())
287
+
288
+
289
+ def _get_agent_dict(agent_url: str, api_key: str) -> dict:
290
+ data = _fetch_settings(agent_url, api_key)
291
+ agent_settings = data.get("agent_settings", {})
292
+ llm = agent_settings.get("llm", {})
293
+ return {
294
+ "kind": "Agent",
295
+ "llm": llm,
296
+ "tools": [{"name": "terminal"}, {"name": "file_editor"}],
297
+ }
298
+
299
+
300
+ def _get_mcp_config(agent_url: str, api_key: str) -> dict | None:
301
+ try:
302
+ data = _fetch_settings(agent_url, api_key)
303
+ mcp_config = data.get("agent_settings", {}).get("mcp_config")
304
+ if isinstance(mcp_config, dict) and mcp_config.get("mcpServers"):
305
+ return mcp_config
306
+ except Exception as exc:
307
+ print(f"Warning: could not fetch MCP config: {exc}")
308
+ return None
309
+
310
+
311
+ def _list_secret_names(agent_url: str, api_key: str) -> list[dict]:
312
+ try:
313
+ result = _oh_request(agent_url, api_key, "GET", "/api/settings/secrets")
314
+ return result.get("secrets", [])
315
+ except Exception as exc:
316
+ print(f"Warning: could not list secrets: {exc}")
317
+ return []
318
+
319
+
320
+ def _build_secrets_payload(agent_url: str, api_key: str) -> dict:
321
+ secrets_list = _list_secret_names(agent_url, api_key)
322
+ secrets: dict = {}
323
+ for secret in secrets_list:
324
+ name = secret.get("name", "")
325
+ if not name:
326
+ continue
327
+ lookup: dict = {
328
+ "kind": "LookupSecret",
329
+ "url": f"/api/settings/secrets/{name}",
330
+ }
331
+ if api_key:
332
+ lookup["headers"] = {"X-Session-API-Key": api_key}
333
+ desc = secret.get("description")
334
+ if desc:
335
+ lookup["description"] = desc
336
+ secrets[name] = lookup
337
+ return secrets
338
+
339
+
340
+ def create_conversation(agent_url: str, api_key: str, initial_message: str) -> str:
341
+ """Create an OpenHands conversation and return its ID."""
342
+ workspace_dir = os.environ.get("WORKSPACE_BASE", "/workspace")
343
+ agent = _get_agent_dict(agent_url, api_key)
344
+ payload: dict = {
345
+ "workspace": {"working_dir": workspace_dir},
346
+ "agent": agent,
347
+ "initial_message": {"content": [{"text": initial_message}]},
348
+ }
349
+ secrets = _build_secrets_payload(agent_url, api_key)
350
+ if secrets:
351
+ payload["secrets"] = secrets
352
+ mcp_config = _get_mcp_config(agent_url, api_key)
353
+ if mcp_config:
354
+ payload["mcp_config"] = mcp_config
355
+ result = _oh_request(agent_url, api_key, "POST", "/api/conversations", payload)
356
+ return result["id"]
357
+
358
+
359
+ def conversation_status(agent_url: str, api_key: str, conv_id: str) -> str:
360
+ result = _oh_request(agent_url, api_key, "GET", f"/api/conversations/{conv_id}")
361
+ return result.get("execution_status", "unknown")
362
+
363
+
364
+ def conversation_final_response(agent_url: str, api_key: str, conv_id: str) -> str:
365
+ result = _oh_request(
366
+ agent_url, api_key, "GET",
367
+ f"/api/conversations/{conv_id}/agent_final_response",
368
+ )
369
+ return result.get("response", "")
370
+
371
+
372
+ # ── Prompt building ────────────────────────────────────────────────────────────
373
+
374
+ _TONE_INSTRUCTIONS: dict[str, str] = {
375
+ "thorough": (
376
+ "Provide a comprehensive review. Cover correctness, security vulnerabilities, "
377
+ "missing or inadequate tests, code style, maintainability, and potential edge "
378
+ "cases. Reference specific files and line numbers where relevant."
379
+ ),
380
+ "concise": (
381
+ "Provide a brief, high-signal review. Focus only on the most important issues — "
382
+ "bugs, security problems, or significant design flaws. Omit minor style feedback."
383
+ ),
384
+ "friendly": (
385
+ "Provide a constructive, encouraging review. Acknowledge what is done well before "
386
+ "raising concerns. Be positive and supportive while still noting real issues."
387
+ ),
388
+ }
389
+
390
+
391
+ def _build_review_prompt(pr: dict, diff: str, diff_truncated: bool) -> str:
392
+ number = pr.get("number", "?")
393
+ title = pr.get("title", "(no title)")
394
+ body = (pr.get("body") or "").strip() or "(no description)"
395
+ html_url = pr.get("html_url", "")
396
+ author = (pr.get("user") or {}).get("login", "?")
397
+ base_branch = (pr.get("base") or {}).get("ref", "?")
398
+ head_branch = (pr.get("head") or {}).get("ref", "?")
399
+ labels = [lb["name"] for lb in (pr.get("labels") or [])]
400
+ label_str = ", ".join(labels) if labels else "(none)"
401
+ changed_files = pr.get("changed_files", "?")
402
+ additions = pr.get("additions", "?")
403
+ deletions = pr.get("deletions", "?")
404
+
405
+ tone = _TONE_INSTRUCTIONS.get(REVIEW_TONE, _TONE_INSTRUCTIONS["thorough"])
406
+ extra = (
407
+ f"\n\nAdditional style instructions:\n{REVIEW_STYLE_INSTRUCTIONS}"
408
+ if REVIEW_STYLE_INSTRUCTIONS.strip()
409
+ else ""
410
+ )
411
+ truncation_note = (
412
+ f"\n\n⚠️ The diff below has been truncated to the first {MAX_DIFF_LINES} lines. "
413
+ "Review what is available and note that the full diff is larger."
414
+ ) if diff_truncated else ""
415
+
416
+ return (
417
+ f"You are an AI code reviewer. Review the following GitHub pull request "
418
+ f"and write a review comment.\n\n"
419
+ f"Repository : {REPO}\n"
420
+ f"PR #{number}: \"{title}\"\n"
421
+ f"Author : @{author}\n"
422
+ f"Base → Head: {base_branch} ← {head_branch}\n"
423
+ f"Labels : {label_str}\n"
424
+ f"Changes : +{additions} -{deletions} across {changed_files} file(s)\n"
425
+ f"URL : {html_url}\n"
426
+ f"\nPR Description:\n---\n{body}\n---\n"
427
+ f"{truncation_note}"
428
+ f"\nDiff:\n```diff\n{diff}\n```\n"
429
+ f"\nReview instructions:\n{tone}{extra}\n\n"
430
+ f"Output ONLY the review text — no preamble, no meta-commentary. "
431
+ f"This text will be posted verbatim as a comment on the pull request.\n"
432
+ f"End your review with a clear verdict on its own line: "
433
+ f"either `✅ APPROVED` or `🔄 CHANGES REQUESTED`."
434
+ )
435
+
436
+
437
+ # ── Core logic ─────────────────────────────────────────────────────────────────
438
+
439
+ def _process_new_pr(
440
+ github_token: str,
441
+ agent_url: str,
442
+ api_key: str,
443
+ openhands_url: str,
444
+ pr: dict,
445
+ conversations: dict,
446
+ ) -> str | None:
447
+ """Start a review conversation for a PR not yet seen. Returns the conversation ID."""
448
+ number = pr["number"]
449
+ title = pr.get("title", "(no title)")
450
+ html_url = pr.get("html_url", "")
451
+ print(f" New PR #{number}: \"{title}\"")
452
+
453
+ try:
454
+ diff = _get_pr_diff(github_token, REPO, number)
455
+ except Exception as exc:
456
+ print(f" Warning: could not fetch diff for PR #{number}: {exc}")
457
+ return None
458
+
459
+ diff_lines = diff.splitlines()
460
+
461
+ if len(diff_lines) > MAX_DIFF_LINES_SKIP:
462
+ print(f" Skipping PR #{number}: diff too large ({len(diff_lines)} lines)")
463
+ conversations[str(number)] = {
464
+ "pr_number": number,
465
+ "html_url": html_url,
466
+ "status": "skipped",
467
+ "reason": f"diff too large ({len(diff_lines)} lines)",
468
+ "last_activity": time.time(),
469
+ }
470
+ _post_github_comment(
471
+ github_token, REPO, number,
472
+ f"⚠️ **OpenHands PR Reviewer**: This PR's diff is too large to review "
473
+ f"automatically ({len(diff_lines):,} lines). Consider splitting it into "
474
+ f"smaller PRs.\n\n_This message was posted by an AI agent (OpenHands)._",
475
+ )
476
+ return None
477
+
478
+ diff_truncated = len(diff_lines) > MAX_DIFF_LINES
479
+ if diff_truncated:
480
+ diff = "\n".join(diff_lines[:MAX_DIFF_LINES])
481
+
482
+ prompt = _build_review_prompt(pr, diff, diff_truncated)
483
+
484
+ try:
485
+ conv_id = create_conversation(agent_url, api_key, prompt)
486
+ except Exception as exc:
487
+ print(f" Error creating conversation for PR #{number}: {exc}")
488
+ return None
489
+
490
+ conversations[str(number)] = {
491
+ "pr_number": number,
492
+ "html_url": html_url,
493
+ "status": "active",
494
+ "conversation_id": conv_id,
495
+ "last_activity": time.time(),
496
+ }
497
+ print(f" Created review conversation {conv_id}")
498
+
499
+ conv_url = f"{openhands_url}/conversations/{conv_id}"
500
+ _post_github_comment(
501
+ github_token, REPO, number,
502
+ f"🤖 **OpenHands is reviewing this PR.**\n\n"
503
+ f"View the conversation: {conv_url}\n\n"
504
+ f"_This comment was posted by an AI agent (OpenHands)._",
505
+ )
506
+ return conv_id
507
+
508
+
509
+ def _check_conversation_completion(
510
+ rec: dict,
511
+ github_token: str,
512
+ agent_url: str,
513
+ api_key: str,
514
+ ) -> None:
515
+ """Post the review result and close the conversation record once it finishes."""
516
+ if (time.time() - rec.get("last_activity", 0.0)) < DONE_DEBOUNCE:
517
+ return
518
+
519
+ conv_id = rec["conversation_id"]
520
+ pr_number = rec["pr_number"]
521
+
522
+ try:
523
+ status = conversation_status(agent_url, api_key, conv_id)
524
+ except Exception as exc:
525
+ print(f" Warning: could not get status for {conv_id}: {exc}")
526
+ return
527
+
528
+ print(f" PR #{pr_number} conversation {conv_id} → status={status}")
529
+
530
+ if status not in ("idle", "finished", "error", "stuck"):
531
+ return
532
+
533
+ try:
534
+ final = conversation_final_response(agent_url, api_key, conv_id)
535
+ except Exception:
536
+ final = ""
537
+
538
+ if status in ("error", "stuck"):
539
+ comment_body = (
540
+ f"⚠️ **OpenHands PR Reviewer encountered a problem** (status: `{status}`).\n\n"
541
+ + (f"{final}\n\n" if final else "")
542
+ + "_This message was posted by an AI agent (OpenHands)._"
543
+ )
544
+ else:
545
+ comment_body = (
546
+ final if final
547
+ else (
548
+ "✅ **OpenHands completed the review.** (No review text was produced.)\n\n"
549
+ "_This message was posted by an AI agent (OpenHands)._"
550
+ )
551
+ )
552
+
553
+ _post_github_comment(github_token, REPO, pr_number, comment_body)
554
+ rec["status"] = "closed"
555
+ print(f" Posted review for PR #{pr_number}")
556
+
557
+
558
+ # ── Main ───────────────────────────────────────────────────────────────────────
559
+
560
+ def main() -> str | None:
561
+ """Run one polling cycle. Returns the last conversation ID created, if any."""
562
+ state_path = _state_file_path()
563
+ state = load_state(state_path)
564
+
565
+ agent_url = os.environ.get("AGENT_SERVER_URL", "").rstrip("/")
566
+ api_key = _get_env_key()
567
+
568
+ github_token = _resolve_github_token()
569
+ _verify_token_and_repo(github_token, REPO)
570
+
571
+ try:
572
+ openhands_url = get_secret("OPENHANDS_URL").rstrip("/") or DEFAULT_OPENHANDS_URL
573
+ except Exception:
574
+ openhands_url = DEFAULT_OPENHANDS_URL
575
+
576
+ conversations: dict = state.get("conversations", {})
577
+
578
+ try:
579
+ open_prs = _list_open_prs(github_token, REPO)
580
+ except Exception as exc:
581
+ raise RuntimeError(f"Failed to list open PRs for {REPO}: {exc}") from exc
582
+
583
+ print(f"Found {len(open_prs)} open PR(s) in {REPO}")
584
+
585
+ open_pr_numbers = {str(pr["number"]) for pr in open_prs}
586
+
587
+ last_conversation_id: str | None = None
588
+
589
+ # Queue reviews for PRs not yet in state.
590
+ for pr in open_prs:
591
+ key = str(pr["number"])
592
+ if key in conversations:
593
+ continue
594
+ conv_id = _process_new_pr(
595
+ github_token, agent_url, api_key, openhands_url, pr, conversations,
596
+ )
597
+ if conv_id:
598
+ last_conversation_id = conv_id
599
+
600
+ # Check active conversations for completion.
601
+ for key, rec in list(conversations.items()):
602
+ if rec.get("status") != "active":
603
+ continue
604
+ if key not in open_pr_numbers:
605
+ # PR was closed/merged before review completed — mark closed silently.
606
+ rec["status"] = "closed"
607
+ print(f" PR #{rec.get('pr_number')} closed/merged — skipping result post")
608
+ continue
609
+ _check_conversation_completion(rec, github_token, agent_url, api_key)
610
+
611
+ state["conversations"] = conversations
612
+ save_state(state_path, state)
613
+ print(f"State saved → {state_path}")
614
+ return last_conversation_id
615
+
616
+
617
+ if __name__ == "__main__":
618
+ try:
619
+ conversation_id = main()
620
+ fire_callback("COMPLETED", conversation_id=conversation_id)
621
+ except Exception as exc:
622
+ import traceback
623
+ traceback.print_exc()
624
+ fire_callback("FAILED", str(exc))
625
+ sys.exit(1)
package/skills/index.js CHANGED
@@ -185,7 +185,7 @@ export const SKILLS_CATALOG = [
185
185
  "triggers": [
186
186
  "/pr-reviewer:setup"
187
187
  ],
188
- "content": "# GitHub PR Reviewer Automation\n\nSet up a cron or event-triggered automation that reviews GitHub pull requests.\n\n---\n\n## Prerequisites\n\n### Required integration\n\n- **GitHub MCP** must be installed in Settings → MCP.\n\n### Information to collect\n\nAsk the user for:\n\n1. **Repositories** — which repos should be watched (e.g. `myorg/backend`, `myorg/frontend`)\n2. **Trigger type** — event-based (reacts to `pull_request.opened` / `pull_request.synchronize`) or cron-based polling\n3. **Review tone** — thorough, concise, or friendly\n4. **Auto-post or draft** — should reviews be posted as GitHub comments automatically, or saved as draft artifacts for human approval first?\n5. **Filters** — any label, branch, or author filters (e.g. skip drafts, only review PRs with `needs-review` label)\n\n---\n\n## Setup Workflow\n\n### Step 1 — Verify GitHub MCP access\n\nConfirm the GitHub MCP integration is working:\n```\nUse the GitHub MCP to list recent pull requests in one of the target repositories.\n```\n\nIf it fails, tell the user to install the GitHub MCP integration first.\n\n### Step 2 — Determine trigger type\n\n**Event-based (recommended if publicly reachable):**\nCheck `<RUNTIME_SERVICES>` for deployment reachability. If the deployment has a public URL, recommend an event trigger on `pull_request.opened` and `pull_request.synchronize`.\n\n**Cron-based (local/private deployments):**\nIf the deployment is local, set up a cron schedule (e.g. every 5 minutes) that polls for recently updated PRs.\n\n### Step 3 — Build the review prompt\n\nConstruct a prompt for the automation that includes:\n- The user's chosen repositories\n- Review focus areas (correctness, security, tests, maintainability)\n- Review tone preference\n- Whether to auto-post or draft\n\n### Step 4 — Create the automation\n\nRead the Automation backend URL and auth from `<RUNTIME_SERVICES>`:\n- Use the **Automation backend** `url_from_agent` as `OPENHANDS_HOST`\n- Auth: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\nUse the **prompt preset** endpoint:\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1/preset/prompt\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"GitHub PR Reviewer\",\n \"prompt\": \"<constructed review prompt>\",\n \"trigger\": <trigger config from step 2>\n }'\n```\n\n### Step 5 — Confirm\n\nTell the user:\n> ✅ **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repositories: `{repo list}`\n> - Trigger: `{trigger description}`\n> - Review style: `{tone}`, `{auto-post or draft}`"
188
+ "content": "# GitHub PR Reviewer Automation\n\nCreate a cron automation that polls a GitHub repository, reviews each open\npull request exactly once, and posts the AI review as a GitHub comment.\n\nThe automation script is fully deterministic: PR discovery, state tracking,\nand deduplication are handled in Python. The LLM is only invoked to write\nthe review text for PRs not yet seen, never for orchestration.\n\n---\n\n## Prerequisites\n\n### Required secret\n\nVerify that the following secret is set in **OpenHands Settings → Secrets**:\n\n| Secret name | Token type | Minimum permissions |\n|---|---|---|\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Classic PAT | `repo` (private) or `public_repo` (public) |\n| `GITHUB_PERSONAL_ACCESS_TOKEN` | Fine-grained PAT | Pull requests: Read and Write |\n\nCheck with:\n```bash\ncurl -s https://api.github.com/user \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('login') or d.get('message'))\"\n```\n\nIf the token is missing or invalid, inform the user and stop.\n\n---\n\n## Setup Workflow\n\nFollow these steps in order.\n\n### Step 1 — Verify GITHUB_PERSONAL_ACCESS_TOKEN\n\nRun the `curl` check above.\n\n- If absent: *\"GITHUB_PERSONAL_ACCESS_TOKEN is not set. Please add it in\n OpenHands Settings → Secrets.\"* Stop.\n- If the API returns `{\"message\": \"Bad credentials\"}`: tell the user the\n token is invalid and ask them to update it. Stop.\n\n### Step 2 — Collect repository\n\nAsk: *\"Which GitHub repository should be monitored?\n(Format: `owner/repo`, e.g. `myorg/backend`)\"*\n\nValidate access:\n```bash\ncurl -s \"https://api.github.com/repos/{owner}/{repo}\" \\\n -H \"Authorization: Bearer $GITHUB_PERSONAL_ACCESS_TOKEN\" \\\n | python3 -c \"\nimport json, sys\nd = json.load(sys.stdin)\nif 'message' in d:\n print('ERROR:', d['message'])\nelse:\n print(f\\\"Accessible. Private: {d.get('private')}. Permissions: {d.get('permissions')}\\\")\n\"\n```\n\nRecord `REPO = \"{owner}/{repo}\"`.\n\n### Step 3 — Collect review tone\n\nAsk: *\"What review tone should the reviewer use?\n 1. Thorough (default) — comprehensive coverage of correctness, security, tests, style\n 2. Concise — high-signal only, skips minor style feedback\n 3. Friendly — constructive and encouraging\n(Press Enter for Thorough, or type your choice or any custom style description)\"*\n\nMap the choice to `REVIEW_TONE`:\n\n| Answer | `REVIEW_TONE` | `REVIEW_STYLE_INSTRUCTIONS` |\n|--------|--------------|------------------------------|\n| 1 / Enter | `\"thorough\"` | `\"\"` |\n| 2 | `\"concise\"` | `\"\"` |\n| 3 | `\"friendly\"` | `\"\"` |\n| Custom text (e.g. \"hostile pirate\") | `\"thorough\"` | the custom text verbatim |\n\n### Step 4 — Collect cron schedule\n\nAsk: *\"How often should the automation poll for new PRs?\n(Press Enter for the default: every 5 minutes.\nUse a cron expression for a different interval, e.g. `0 * * * *` = hourly)\"*\n\nDefault: `*/5 * * * *`.\n\nRecord as `CRON_SCHEDULE`.\n\n### Step 5 — Generate the automation script\n\nRead `scripts/main.py` from this skill's directory. Apply exactly four\nconstant substitutions near the top of the file:\n\n| Placeholder | Replace with |\n|---|---|\n| `REPO = \"owner/repo\"` | `REPO = \"{owner_repo}\"` |\n| `REVIEW_TONE = \"thorough\"` | `REVIEW_TONE = \"{review_tone}\"` |\n| `REVIEW_STYLE_INSTRUCTIONS = \"\"` | `REVIEW_STYLE_INSTRUCTIONS = \"{style_instructions}\"` |\n| `DEFAULT_OPENHANDS_URL = \"http://localhost:8000\"` | leave unchanged unless the user has a preference |\n\nWrite the customised script to a temporary build directory:\n```bash\nmkdir -p /tmp/pr-reviewer-build\n# write the customised main.py to /tmp/pr-reviewer-build/main.py\n```\n\nValidate syntax before packaging:\n```bash\npython3 -m py_compile /tmp/pr-reviewer-build/main.py && echo \"Syntax OK\"\n```\n\nFix any syntax errors before proceeding.\n\n### Step 6 — Package and upload\n\nDetermine the Automation backend URL and auth from the `<RUNTIME_SERVICES>`\nblock in your system context:\n- **OPENHANDS_HOST**: the Automation backend `url_from_agent`\n- **Auth**: `X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY`\n\n```bash\ntar -czf /tmp/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .\n\nTARBALL_PATH=$(curl -s -X POST \\\n \"${OPENHANDS_HOST}/api/automation/v1/uploads?name=github-pr-reviewer\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/gzip\" \\\n --data-binary @/tmp/pr-reviewer.tar.gz \\\n | python3 -c \"import json,sys; print(json.load(sys.stdin)['tarball_path'])\")\n\necho \"Uploaded: $TARBALL_PATH\"\n```\n\n### Step 7 — Register the automation\n\n```bash\ncurl -s -X POST \"${OPENHANDS_HOST}/api/automation/v1\" \\\n -H \"X-Session-API-Key: $OPENHANDS_AUTOMATION_API_KEY\" \\\n -H \"Content-Type: application/json\" \\\n -d \"{\n \\\"name\\\": \\\"GitHub PR Reviewer: {owner}/{repo}\\\",\n \\\"trigger\\\": {\\\"type\\\": \\\"cron\\\", \\\"schedule\\\": \\\"{cron_schedule}\\\"},\n \\\"tarball_path\\\": \\\"$TARBALL_PATH\\\",\n \\\"entrypoint\\\": \\\"python3 main.py\\\",\n \\\"timeout\\\": 300\n }\" | python3 -m json.tool\n```\n\nRecord the returned `id`.\n\n### Step 8 — Confirm\n\nTell the user:\n\n> ✅ **GitHub PR Reviewer** is running!\n>\n> - Automation ID: `{id}`\n> - Repository: `{owner}/{repo}`\n> - Review tone: `{tone}`\n> - Polling schedule: `{cron_schedule}`\n> - State file: `~/.openhands/workspaces/automation-state/github_pr_reviewer_{id}.json`\n>\n> The next cron run will discover all currently open PRs and queue reviews.\n> Each PR is reviewed exactly once; state is stored in the JSON file above.\n> To force a re-review of all PRs, delete the state file.\n\n---\n\n## Runtime Behaviour (per poll)\n\nEach cron run executes `main.py`, which:\n\n1. **Loads state** from the JSON file (see `references/state-schema.md`).\n2. **Resolves and validates `GITHUB_PERSONAL_ACCESS_TOKEN`** — aborts\n immediately if absent or invalid.\n3. **Lists all open PRs** in the configured repository.\n4. **For each PR not yet in state**:\n - Fetches the unified diff via the GitHub API.\n - Skips PRs whose diff exceeds `MAX_DIFF_LINES_SKIP` (default 5000 lines)\n and posts an explanatory comment.\n - Truncates diffs larger than `MAX_DIFF_LINES` (default 500 lines) and\n notes this in the prompt.\n - Creates an OpenHands conversation with the PR metadata, diff, and\n configured tone instructions as the initial message.\n - Posts an acknowledgement comment on the PR with a link to the conversation.\n - Records the PR in state with `status: \"active\"`.\n5. **For each active conversation**:\n - Skips PRs that have been closed or merged (marks them closed silently).\n - Checks the conversation's `execution_status`.\n - When it reaches `idle`, `finished`, `error`, or `stuck`:\n posts the agent's final response as a GitHub comment and marks the\n conversation `closed`.\n6. **Saves updated state** and fires the completion callback.\n\n---\n\n## Additional Resources\n\n- **`references/state-schema.md`** — State JSON schema, field definitions,\n and conversation lifecycle diagram.\n- **`scripts/main.py`** — The complete automation script. Customise the\n four constants at the top before packaging.\n\n---\n\n## Troubleshooting\n\n| Symptom | Likely cause | Fix |\n|---|---|---|\n| Bot never posts reviews | `GITHUB_PERSONAL_ACCESS_TOKEN` missing or wrong scopes | Verify token; check Step 1 |\n| \"Bad credentials\" in run logs | Token expired | Rotate and update the secret |\n| 404 on repo access | Repo name wrong or no access | Re-check `owner/repo` and token permissions |\n| Same PR reviewed twice | State file deleted or corrupted | Check that the state file path is stable across runs |\n| Review never posted | Conversation stuck in `running` | Open the conversation in the OpenHands UI |\n| PR skipped silently | Diff too large | Raise `MAX_DIFF_LINES_SKIP` in the script or split the PR |"
189
189
  },
190
190
  {
191
191
  "name": "github-repo-monitor",
@@ -327,7 +327,7 @@ export const SKILLS_CATALOG = [
327
327
  "agent-sdk",
328
328
  "/sdk"
329
329
  ],
330
- "content": "# OpenHands Software Agent SDK\n\nAll SDK documentation lives at <https://docs.openhands.dev/sdk>.\n\nFor the full topic index, fetch <https://docs.openhands.dev/llms.txt> and read\nthe \"OpenHands Software Agent SDK\" section.\n\n## Quick reference\n\nInstall: `pip install openhands-sdk openhands-tools`\n\n```python\nimport os\n\nfrom openhands.sdk import LLM, Agent, Conversation, Tool\nfrom openhands.tools.file_editor import FileEditorTool\nfrom openhands.tools.task_tracker import TaskTrackerTool\nfrom openhands.tools.terminal import TerminalTool\n\n\nllm = LLM(\n model=os.getenv(\"LLM_MODEL\", \"gpt-5.5\"),\n api_key=os.getenv(\"LLM_API_KEY\"),\n base_url=os.getenv(\"LLM_BASE_URL\", None),\n)\n\nagent = Agent(\n llm=llm,\n tools=[\n Tool(name=TerminalTool.name),\n Tool(name=FileEditorTool.name),\n Tool(name=TaskTrackerTool.name),\n ],\n)\n\ncwd = os.getcwd()\nconversation = Conversation(agent=agent, workspace=cwd)\n\nconversation.send_message(\"Write 3 facts about the current project into FACTS.txt.\")\nconversation.run()\nprint(\"All done!\")\n```\n\n## Core classes (`openhands.sdk`)\n\n| Class | Purpose |\n|---|---|\n| [`Agent`](https://docs.openhands.dev/sdk/arch/agent.md) | Reasoning-action loop |\n| [`Condenser`](https://docs.openhands.dev/sdk/arch/condenser.md) | Conversation history compression system |\n| [`Conversation`](https://docs.openhands.dev/sdk/arch/conversation.md) | Conversation orchestration system |\n| [`Event`](https://docs.openhands.dev/sdk/arch/events.md) | Typed event framework |\n| [`LLM`](https://docs.openhands.dev/sdk/arch/llm.md) | Provider-agnostic language model interface |\n| [`SecurityAnalyzer`](https://docs.openhands.dev/sdk/arch/security.md) | Action security analysis and validation |\n| [`Skill`](https://docs.openhands.dev/sdk/arch/skill.md) | Reusable prompt system |\n| [`Tool / ToolDefinition`](https://docs.openhands.dev/sdk/arch/tool-system.md) | Action-observation tool framework |\n| [`Workspace`](https://docs.openhands.dev/sdk/arch/workspace.md) | Execution environment abstraction |\n\n## API reference\n\n[`openhands.sdk.agent`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.agent.md), [`openhands.sdk.conversation`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.conversation.md), [`openhands.sdk.event`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.event.md), [`openhands.sdk.llm`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.llm.md), [`openhands.sdk.security`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.security.md), [`openhands.sdk.tool`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.tool.md), [`openhands.sdk.utils`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.utils.md), [`openhands.sdk.workspace`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.workspace.md)\n\n## Guides\n\n- [ACP Agent](https://docs.openhands.dev/sdk/guides/agent-acp.md): Delegate to an ACP-compatible server (Claude Code, Gemini CLI, etc.) instead of calling an LLM directly.\n- [Agent Settings](https://docs.openhands.dev/sdk/guides/agent-settings.md): Configure, serialize, and recreate agents from structured settings.\n- [Agent Skills & Context](https://docs.openhands.dev/sdk/guides/skill.md): Skills add specialized behaviors, domain knowledge, and context-aware triggers to your agent through structured prompts.\n- [API-based Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/api-sandbox.md): Connect to hosted API-based agent server for fully managed infrastructure.\n- [Apptainer Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/apptainer-sandbox.md): Run agent server in rootless Apptainer containers for HPC and shared computing environments.\n- [Ask Agent Questions](https://docs.openhands.dev/sdk/guides/convo-ask-agent.md): Get sidebar replies from the agent during conversation execution without interrupting the main flow.\n- [Assign Reviews](https://docs.openhands.dev/sdk/guides/github-workflows/assign-reviews.md): Automate PR management with intelligent reviewer assignment and workflow notifications using OpenHands Agent\n- [Browser Session Recording](https://docs.openhands.dev/sdk/guides/browser-session-recording.md): Record and replay your agent's browser sessions using rrweb.\n- [Browser Use](https://docs.openhands.dev/sdk/guides/agent-browser-use.md): Enable web browsing and interaction capabilities for your agent.\n- [Context Condenser](https://docs.openhands.dev/sdk/guides/context-condenser.md): Manage agent memory by condensing conversation history to save tokens.\n- [Conversation with Async](https://docs.openhands.dev/sdk/guides/convo-async.md): Use async/await for concurrent agent operations and non-blocking execution.\n- [Creating Custom Agent](https://docs.openhands.dev/sdk/guides/agent-custom.md): Learn how to design specialized agents with custom tool sets\n- [Critic (Experimental)](https://docs.openhands.dev/sdk/guides/critic.md): Real-time evaluation of agent actions using an LLM-based critic model, with built-in iterative refinement.\n- [Custom Tools](https://docs.openhands.dev/sdk/guides/custom-tools.md): Tools define what agents can do. The SDK includes built-in tools for common operations and supports creating custom tools for specialized needs.\n- [Custom Tools with Remote Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/custom-tools.md): Learn how to use custom tools with a remote agent server by building a custom base image that includes your tool implementations.\n- [Custom Visualizer](https://docs.openhands.dev/sdk/guides/convo-custom-visualizer.md): Customize conversation visualization by creating custom visualizers or configuring the default visualizer.\n- [Docker Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/docker-sandbox.md): Run agent server in isolated Docker containers for security and reproducibility.\n- [Exception Handling](https://docs.openhands.dev/sdk/guides/llm-error-handling.md): Provider‑agnostic exceptions raised by the SDK and recommended patterns for handling them.\n- [FAQ](https://docs.openhands.dev/sdk/faq.md): Frequently asked questions about the OpenHands SDK\n- [File-Based Agents](https://docs.openhands.dev/sdk/guides/agent-file-based.md): Define specialized sub-agents as simple Markdown files with YAML frontmatter — no Python code required.\n- [Fork a Conversation](https://docs.openhands.dev/sdk/guides/convo-fork.md): Branch off an existing conversation for follow-up exploration without contaminating the original.\n- [Getting Started](https://docs.openhands.dev/sdk/getting-started.md): Install the OpenHands SDK and build AI agents that write software.\n- [GPT-5 Preset (ApplyPatchTool)](https://docs.openhands.dev/sdk/guides/llm-gpt5-preset.md): Use the GPT-5 preset to build an agent that swaps the standard FileEditorTool for ApplyPatchTool.\n- [Hello World](https://docs.openhands.dev/sdk/guides/hello-world.md): The simplest possible OpenHands agent - configure an LLM, create an agent, and complete a task.\n- [Hooks](https://docs.openhands.dev/sdk/guides/hooks.md): Use lifecycle hooks to observe, log, and customize agent execution.\n- [Image Input](https://docs.openhands.dev/sdk/guides/llm-image-input.md): Send images to multimodal agents for vision-based tasks and analysis.\n- [Interactive Terminal](https://docs.openhands.dev/sdk/guides/agent-interactive-terminal.md): Enable agents to interact with terminal applications like ipython, python REPL, and other interactive CLI tools.\n- [Iterative Refinement](https://docs.openhands.dev/sdk/guides/iterative-refinement.md): Implement iterative refinement workflows where agents refine their work based on critique feedback until quality thresholds are met.\n- [LLM Fallback Strategy](https://docs.openhands.dev/sdk/guides/llm-fallback.md): Automatically try alternate LLMs when the primary model fails with a transient error.\n- [LLM Profile Store](https://docs.openhands.dev/sdk/guides/llm-profile-store.md): Save, load, and manage reusable LLM configurations so you never repeat setup code again.\n- [LLM Registry](https://docs.openhands.dev/sdk/guides/llm-registry.md): Dynamically select and configure language models using the LLM registry.\n- [LLM Streaming](https://docs.openhands.dev/sdk/guides/llm-streaming.md): Stream LLM responses token-by-token for real-time display and interactive user experiences.\n- [LLM Subscriptions](https://docs.openhands.dev/sdk/guides/llm-subscriptions.md): Use your ChatGPT Plus/Pro subscription to access Codex models without consuming API credits.\n- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Run agents through a local HTTP server with RemoteConversation for client-server architecture.\n- [Metrics Tracking](https://docs.openhands.dev/sdk/guides/metrics.md): Track token usage, costs, and latency metrics for your agents.\n- [Model Context Protocol](https://docs.openhands.dev/sdk/guides/mcp.md): Model Context Protocol (MCP) enables dynamic tool integration from external servers. Agents can discover and use MCP-provided tools automatically.\n- [Model Routing](https://docs.openhands.dev/sdk/guides/llm-routing.md): Route agent's LLM requests to different models.\n- [Observability & Tracing](https://docs.openhands.dev/sdk/guides/observability.md): Enable OpenTelemetry tracing to monitor and debug your agent's execution with tools like Laminar, MLflow, Honeycomb, or any OTLP-compatible backend.\n- [OpenHands Cloud Workspace](https://docs.openhands.dev/sdk/guides/agent-server/cloud-workspace.md): Connect to OpenHands Cloud for fully managed sandbox environments with optional SaaS credential inheritance.\n- [Overview](https://docs.openhands.dev/sdk/guides/agent-server/overview.md): Run agents on remote servers with isolated workspaces for production deployments.\n- [Parallel Tool Execution](https://docs.openhands.dev/sdk/guides/parallel-tool-execution.md): Execute multiple tools concurrently within a single LLM response to improve throughput for independent operations.\n- [Pause and Resume](https://docs.openhands.dev/sdk/guides/convo-pause-and-resume.md): Pause agent execution, perform operations, and resume without losing state.\n- [Persistence](https://docs.openhands.dev/sdk/guides/convo-persistence.md): Save and restore conversation state for multi-session workflows.\n- [Plugins](https://docs.openhands.dev/sdk/guides/plugins.md): Plugins bundle skills, hooks, MCP servers, agents, and commands into reusable packages that extend agent capabilities.\n- [PR Review](https://docs.openhands.dev/sdk/guides/github-workflows/pr-review.md): Use OpenHands Agent to generate meaningful pull request review\n- [Reasoning](https://docs.openhands.dev/sdk/guides/llm-reasoning.md): Access model reasoning traces from Anthropic extended thinking and OpenAI responses API.\n- [Secret Registry](https://docs.openhands.dev/sdk/guides/secrets.md): Provide environment variables and secrets to agent workspace securely.\n- [Security & Action Confirmation](https://docs.openhands.dev/sdk/guides/security.md): Control agent action execution through confirmation policy and security analyzer.\n- [Send Message While Running](https://docs.openhands.dev/sdk/guides/convo-send-message-while-running.md): Interrupt running agents to provide additional context or corrections.\n- [Software Agent SDK](https://docs.openhands.dev/sdk.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.\n- [Stuck Detector](https://docs.openhands.dev/sdk/guides/agent-stuck-detector.md): Detect and handle stuck agents automatically with timeout mechanisms.\n- [Sub-Agent Delegation](https://docs.openhands.dev/sdk/guides/agent-delegation.md): Enable parallel task execution by delegating work to multiple sub-agents that run independently and return consolidated results.\n- [Task Tool Set](https://docs.openhands.dev/sdk/guides/task-tool-set.md): Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent.\n- [Theory of Mind (TOM) Agent](https://docs.openhands.dev/sdk/guides/agent-tom-agent.md): Enable your agent to understand user intent and preferences through Theory of Mind capabilities, providing personalized guidance based on user modeling.\n- [TODO Management](https://docs.openhands.dev/sdk/guides/github-workflows/todo-management.md): Implement TODOs using OpenHands Agent\n\n## Examples\n\nSource: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples)\n\n### [`01_standalone_sdk/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk)\n\n- [`01_hello_world.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/01_hello_world.py)\n- [`02_custom_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/02_custom_tools.py)\n- [`03_activate_skill.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/03_activate_skill.py)\n- [`04_confirmation_mode_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/04_confirmation_mode_example.py)\n- [`05_use_llm_registry.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/05_use_llm_registry.py)\n- [`06_interactive_terminal_w_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py)\n- [`07_mcp_integration.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/07_mcp_integration.py)\n- [`08_mcp_with_oauth.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/08_mcp_with_oauth.py)\n- [`09_pause_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/09_pause_example.py)\n- [`10_persistence.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/10_persistence.py)\n- [`11_async.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/11_async.py)\n- [`12_custom_secrets.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/12_custom_secrets.py)\n- [`13_get_llm_metrics.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/13_get_llm_metrics.py)\n- [`14_context_condenser.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/14_context_condenser.py)\n- [`15_browser_use.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/15_browser_use.py)\n- [`16_llm_security_analyzer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/16_llm_security_analyzer.py)\n- [`17_image_input.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/17_image_input.py)\n- [`18_send_message_while_processing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/18_send_message_while_processing.py)\n- [`19_llm_routing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/19_llm_routing.py)\n- [`20_stuck_detector.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_stuck_detector.py)\n- [`21_generate_extraneous_conversation_costs.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/21_generate_extraneous_conversation_costs.py)\n- [`22_anthropic_thinking.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/22_anthropic_thinking.py)\n- [`23_responses_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/23_responses_reasoning.py)\n- [`24_planning_agent_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/24_planning_agent_workflow.py)\n- [`25_agent_delegation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/25_agent_delegation.py)\n- [`26_custom_visualizer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/26_custom_visualizer.py)\n- [`27_observability_laminar.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/27_observability_laminar.py)\n- [`28_ask_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/28_ask_agent_example.py)\n- [`29_llm_streaming.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/29_llm_streaming.py)\n- [`30_tom_agent.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/30_tom_agent.py)\n- [`31_iterative_refinement.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/31_iterative_refinement.py)\n- [`32_configurable_security_policy.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/32_configurable_security_policy.py)\n- [`33_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/33_hooks)\n- [`34_critic_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/34_critic_example.py)\n- [`35_subscription_login.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/35_subscription_login.py)\n- [`36_event_json_to_openai_messages.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/36_event_json_to_openai_messages.py)\n- [`37_llm_profile_store`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/37_llm_profile_store)\n- [`38_browser_session_recording.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/38_browser_session_recording.py)\n- [`39_llm_fallback.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/39_llm_fallback.py)\n- [`40_acp_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_acp_agent_example.py)\n- [`41_task_tool_set.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py)\n- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)\n- [`43_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/43_mixed_marketplace_skills)\n- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)\n- [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)\n- [`46_agent_settings.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/46_agent_settings.py)\n- [`47_defense_in_depth_security.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/47_defense_in_depth_security.py)\n- [`48_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/48_conversation_fork.py)\n- [`49_switch_llm_tool.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/49_switch_llm_tool.py)\n- [`50_async_cancellation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/50_async_cancellation.py)\n- [`51_agent_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/51_agent_hooks)\n- [`52_dynamic_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/52_dynamic_workflow.py)\n\n### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)\n\n- [`01_convo_with_local_agent_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/01_convo_with_local_agent_server.py)\n- [`02_convo_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py)\n- [`03_browser_use_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py)\n- [`04_convo_with_api_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py)\n- [`05_vscode_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py)\n- [`06_custom_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/06_custom_tool)\n- [`07_convo_with_cloud_workspace.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/07_convo_with_cloud_workspace.py)\n- [`08_convo_with_apptainer_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py)\n- [`09_acp_agent_with_remote_runtime.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py)\n- [`10_cloud_workspace_share_credentials.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py)\n- [`11_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/11_conversation_fork.py)\n- [`12_settings_and_secrets_api.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/12_settings_and_secrets_api.py)\n- [`13_workspace_get_llm.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/13_workspace_get_llm.py)\n- [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)\n- [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)\n\n### [`03_github_workflows/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows)\n\n- [`01_basic_action`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/01_basic_action)\n- [`02_pr_review`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/02_pr_review)\n- [`03_todo_management`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/03_todo_management)\n- [`04_datadog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/04_datadog_debugging)\n- [`05_posthog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/05_posthog_debugging)\n\n### [`04_llm_specific_tools/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/04_llm_specific_tools)\n\n- [`01_gpt5_apply_patch_preset.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py)\n- [`02_gemini_file_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/02_gemini_file_tools.py)\n\n### [`05_skills_and_plugins/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins)\n\n- [`01_loading_agentskills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/01_loading_agentskills)\n- [`02_loading_plugins`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/02_loading_plugins)\n- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)"
330
+ "content": "# OpenHands Software Agent SDK\n\nAll SDK documentation lives at <https://docs.openhands.dev/sdk>.\n\nFor the full topic index, fetch <https://docs.openhands.dev/llms.txt> and read\nthe \"OpenHands Software Agent SDK\" section.\n\n## Quick reference\n\nInstall: `pip install openhands-sdk openhands-tools`\n\n```python\nimport os\n\nfrom openhands.sdk import LLM, Agent, Conversation, Tool\nfrom openhands.tools.file_editor import FileEditorTool\nfrom openhands.tools.task_tracker import TaskTrackerTool\nfrom openhands.tools.terminal import TerminalTool\n\n\nllm = LLM(\n model=os.getenv(\"LLM_MODEL\", \"gpt-5.5\"),\n api_key=os.getenv(\"LLM_API_KEY\"),\n base_url=os.getenv(\"LLM_BASE_URL\", None),\n)\n\nagent = Agent(\n llm=llm,\n tools=[\n Tool(name=TerminalTool.name),\n Tool(name=FileEditorTool.name),\n Tool(name=TaskTrackerTool.name),\n ],\n)\n\ncwd = os.getcwd()\nconversation = Conversation(agent=agent, workspace=cwd)\n\nconversation.send_message(\"Write 3 facts about the current project into FACTS.txt.\")\nconversation.run()\nprint(\"All done!\")\n```\n\n## Core classes (`openhands.sdk`)\n\n| Class | Purpose |\n|---|---|\n| [`Agent`](https://docs.openhands.dev/sdk/arch/agent.md) | Reasoning-action loop |\n| [`Condenser`](https://docs.openhands.dev/sdk/arch/condenser.md) | Conversation history compression system |\n| [`Conversation`](https://docs.openhands.dev/sdk/arch/conversation.md) | Conversation orchestration system |\n| [`Event`](https://docs.openhands.dev/sdk/arch/events.md) | Typed event framework |\n| [`LLM`](https://docs.openhands.dev/sdk/arch/llm.md) | Provider-agnostic language model interface |\n| [`SecurityAnalyzer`](https://docs.openhands.dev/sdk/arch/security.md) | Action security analysis and validation |\n| [`Skill`](https://docs.openhands.dev/sdk/arch/skill.md) | Reusable prompt system |\n| [`Tool / ToolDefinition`](https://docs.openhands.dev/sdk/arch/tool-system.md) | Action-observation tool framework |\n| [`Workspace`](https://docs.openhands.dev/sdk/arch/workspace.md) | Execution environment abstraction |\n\n## API reference\n\n[`openhands.sdk.agent`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.agent.md), [`openhands.sdk.conversation`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.conversation.md), [`openhands.sdk.event`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.event.md), [`openhands.sdk.llm`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.llm.md), [`openhands.sdk.security`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.security.md), [`openhands.sdk.tool`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.tool.md), [`openhands.sdk.utils`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.utils.md), [`openhands.sdk.workspace`](https://docs.openhands.dev/sdk/api-reference/openhands.sdk.workspace.md)\n\n## Guides\n\n- [ACP Agent](https://docs.openhands.dev/sdk/guides/agent-acp.md): Delegate to an ACP-compatible server (Claude Code, Gemini CLI, etc.) instead of calling an LLM directly.\n- [Agent Settings](https://docs.openhands.dev/sdk/guides/agent-settings.md): Configure, serialize, and recreate agents from structured settings.\n- [Agent Skills & Context](https://docs.openhands.dev/sdk/guides/skill.md): Skills add specialized behaviors, domain knowledge, and context-aware triggers to your agent through structured prompts.\n- [API-based Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/api-sandbox.md): Connect to hosted API-based agent server for fully managed infrastructure.\n- [Apptainer Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/apptainer-sandbox.md): Run agent server in rootless Apptainer containers for HPC and shared computing environments.\n- [Ask Agent Questions](https://docs.openhands.dev/sdk/guides/convo-ask-agent.md): Get sidebar replies from the agent during conversation execution without interrupting the main flow.\n- [Assign Reviews](https://docs.openhands.dev/sdk/guides/github-workflows/assign-reviews.md): Automate PR management with intelligent reviewer assignment and workflow notifications using OpenHands Agent\n- [Browser Session Recording](https://docs.openhands.dev/sdk/guides/browser-session-recording.md): Record and replay your agent's browser sessions using rrweb.\n- [Browser Use](https://docs.openhands.dev/sdk/guides/agent-browser-use.md): Enable web browsing and interaction capabilities for your agent.\n- [Context Condenser](https://docs.openhands.dev/sdk/guides/context-condenser.md): Manage agent memory by condensing conversation history to save tokens.\n- [Conversation with Async](https://docs.openhands.dev/sdk/guides/convo-async.md): Use async/await for concurrent agent operations and non-blocking execution.\n- [Creating Custom Agent](https://docs.openhands.dev/sdk/guides/agent-custom.md): Learn how to design specialized agents with custom tool sets\n- [Critic (Experimental)](https://docs.openhands.dev/sdk/guides/critic.md): Real-time evaluation of agent actions using an LLM-based critic model, with built-in iterative refinement.\n- [Custom Tools](https://docs.openhands.dev/sdk/guides/custom-tools.md): Tools define what agents can do. The SDK includes built-in tools for common operations and supports creating custom tools for specialized needs.\n- [Custom Tools with Remote Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/custom-tools.md): Learn how to use custom tools with a remote agent server by building a custom base image that includes your tool implementations.\n- [Custom Visualizer](https://docs.openhands.dev/sdk/guides/convo-custom-visualizer.md): Customize conversation visualization by creating custom visualizers or configuring the default visualizer.\n- [Docker Sandbox](https://docs.openhands.dev/sdk/guides/agent-server/docker-sandbox.md): Run agent server in isolated Docker containers for security and reproducibility.\n- [Exception Handling](https://docs.openhands.dev/sdk/guides/llm-error-handling.md): Provider‑agnostic exceptions raised by the SDK and recommended patterns for handling them.\n- [FAQ](https://docs.openhands.dev/sdk/faq.md): Frequently asked questions about the OpenHands SDK\n- [File-Based Agents](https://docs.openhands.dev/sdk/guides/agent-file-based.md): Define specialized sub-agents as simple Markdown files with YAML frontmatter — no Python code required.\n- [Fork a Conversation](https://docs.openhands.dev/sdk/guides/convo-fork.md): Branch off an existing conversation for follow-up exploration without contaminating the original.\n- [Getting Started](https://docs.openhands.dev/sdk/getting-started.md): Install the OpenHands SDK and build AI agents that write software.\n- [GPT-5 Preset (ApplyPatchTool)](https://docs.openhands.dev/sdk/guides/llm-gpt5-preset.md): Use the GPT-5 preset to build an agent that swaps the standard FileEditorTool for ApplyPatchTool.\n- [Hello World](https://docs.openhands.dev/sdk/guides/hello-world.md): The simplest possible OpenHands agent - configure an LLM, create an agent, and complete a task.\n- [Hooks](https://docs.openhands.dev/sdk/guides/hooks.md): Use lifecycle hooks to observe, log, and customize agent execution.\n- [Image Input](https://docs.openhands.dev/sdk/guides/llm-image-input.md): Send images to multimodal agents for vision-based tasks and analysis.\n- [Interactive Terminal](https://docs.openhands.dev/sdk/guides/agent-interactive-terminal.md): Enable agents to interact with terminal applications like ipython, python REPL, and other interactive CLI tools.\n- [Iterative Refinement](https://docs.openhands.dev/sdk/guides/iterative-refinement.md): Implement iterative refinement workflows where agents refine their work based on critique feedback until quality thresholds are met.\n- [LLM Fallback Strategy](https://docs.openhands.dev/sdk/guides/llm-fallback.md): Automatically try alternate LLMs when the primary model fails with a transient error.\n- [LLM Profile Store](https://docs.openhands.dev/sdk/guides/llm-profile-store.md): Save, load, and manage reusable LLM configurations so you never repeat setup code again.\n- [LLM Registry](https://docs.openhands.dev/sdk/guides/llm-registry.md): Dynamically select and configure language models using the LLM registry.\n- [LLM Streaming](https://docs.openhands.dev/sdk/guides/llm-streaming.md): Stream LLM responses token-by-token for real-time display and interactive user experiences.\n- [LLM Subscriptions](https://docs.openhands.dev/sdk/guides/llm-subscriptions.md): Use your ChatGPT Plus/Pro subscription to access Codex models without consuming API credits.\n- [Local Agent Server](https://docs.openhands.dev/sdk/guides/agent-server/local-server.md): Run agents through a local HTTP server with RemoteConversation for client-server architecture.\n- [Metrics Tracking](https://docs.openhands.dev/sdk/guides/metrics.md): Track token usage, costs, and latency metrics for your agents.\n- [Model Context Protocol](https://docs.openhands.dev/sdk/guides/mcp.md): Model Context Protocol (MCP) enables dynamic tool integration from external servers. Agents can discover and use MCP-provided tools automatically.\n- [Model Routing](https://docs.openhands.dev/sdk/guides/llm-routing.md): Route agent's LLM requests to different models.\n- [Observability & Tracing](https://docs.openhands.dev/sdk/guides/observability.md): Enable OpenTelemetry tracing to monitor and debug your agent's execution with tools like Laminar, MLflow, Honeycomb, or any OTLP-compatible backend.\n- [OpenHands Cloud Workspace](https://docs.openhands.dev/sdk/guides/agent-server/cloud-workspace.md): Connect to OpenHands Cloud for fully managed sandbox environments with optional SaaS credential inheritance.\n- [Overview](https://docs.openhands.dev/sdk/guides/agent-server/overview.md): Run agents on remote servers with isolated workspaces for production deployments.\n- [Parallel Tool Execution](https://docs.openhands.dev/sdk/guides/parallel-tool-execution.md): Execute multiple tools concurrently within a single LLM response to improve throughput for independent operations.\n- [Pause and Resume](https://docs.openhands.dev/sdk/guides/convo-pause-and-resume.md): Pause agent execution, perform operations, and resume without losing state.\n- [Persistence](https://docs.openhands.dev/sdk/guides/convo-persistence.md): Save and restore conversation state for multi-session workflows.\n- [Plugins](https://docs.openhands.dev/sdk/guides/plugins.md): Plugins bundle skills, hooks, MCP servers, agents, and commands into reusable packages that extend agent capabilities.\n- [PR Review](https://docs.openhands.dev/sdk/guides/github-workflows/pr-review.md): Use OpenHands Agent to generate meaningful pull request review\n- [Reasoning](https://docs.openhands.dev/sdk/guides/llm-reasoning.md): Access model reasoning traces from Anthropic extended thinking and OpenAI responses API.\n- [Secret Registry](https://docs.openhands.dev/sdk/guides/secrets.md): Provide environment variables and secrets to agent workspace securely.\n- [Security & Action Confirmation](https://docs.openhands.dev/sdk/guides/security.md): Control agent action execution through confirmation policy and security analyzer.\n- [Send Message While Running](https://docs.openhands.dev/sdk/guides/convo-send-message-while-running.md): Interrupt running agents to provide additional context or corrections.\n- [Software Agent SDK](https://docs.openhands.dev/sdk.md): Build AI agents that write software. A clean, modular SDK with production-ready tools.\n- [Stuck Detector](https://docs.openhands.dev/sdk/guides/agent-stuck-detector.md): Detect and handle stuck agents automatically with timeout mechanisms.\n- [Sub-Agent Delegation](https://docs.openhands.dev/sdk/guides/agent-delegation.md): Enable parallel task execution by delegating work to multiple sub-agents that run independently and return consolidated results.\n- [Task Tool Set](https://docs.openhands.dev/sdk/guides/task-tool-set.md): Delegate complex work to specialized sub-agents that run synchronously and return results to the parent agent.\n- [Theory of Mind (TOM) Agent](https://docs.openhands.dev/sdk/guides/agent-tom-agent.md): Enable your agent to understand user intent and preferences through Theory of Mind capabilities, providing personalized guidance based on user modeling.\n- [TODO Management](https://docs.openhands.dev/sdk/guides/github-workflows/todo-management.md): Implement TODOs using OpenHands Agent\n\n## Examples\n\nSource: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples)\n\n### [`01_standalone_sdk/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk)\n\n- [`01_hello_world.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/01_hello_world.py)\n- [`02_custom_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/02_custom_tools.py)\n- [`03_activate_skill.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/03_activate_skill.py)\n- [`04_confirmation_mode_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/04_confirmation_mode_example.py)\n- [`05_use_llm_registry.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/05_use_llm_registry.py)\n- [`06_interactive_terminal_w_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/06_interactive_terminal_w_reasoning.py)\n- [`07_mcp_integration.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/07_mcp_integration.py)\n- [`08_mcp_with_oauth.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/08_mcp_with_oauth.py)\n- [`09_pause_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/09_pause_example.py)\n- [`10_persistence.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/10_persistence.py)\n- [`11_async.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/11_async.py)\n- [`12_custom_secrets.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/12_custom_secrets.py)\n- [`13_get_llm_metrics.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/13_get_llm_metrics.py)\n- [`14_context_condenser.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/14_context_condenser.py)\n- [`15_browser_use.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/15_browser_use.py)\n- [`16_llm_security_analyzer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/16_llm_security_analyzer.py)\n- [`17_image_input.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/17_image_input.py)\n- [`18_send_message_while_processing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/18_send_message_while_processing.py)\n- [`19_llm_routing.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/19_llm_routing.py)\n- [`20_stuck_detector.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/20_stuck_detector.py)\n- [`21_generate_extraneous_conversation_costs.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/21_generate_extraneous_conversation_costs.py)\n- [`22_anthropic_thinking.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/22_anthropic_thinking.py)\n- [`23_responses_reasoning.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/23_responses_reasoning.py)\n- [`24_planning_agent_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/24_planning_agent_workflow.py)\n- [`25_agent_delegation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/25_agent_delegation.py)\n- [`26_custom_visualizer.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/26_custom_visualizer.py)\n- [`27_observability_laminar.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/27_observability_laminar.py)\n- [`28_ask_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/28_ask_agent_example.py)\n- [`29_llm_streaming.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/29_llm_streaming.py)\n- [`30_tom_agent.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/30_tom_agent.py)\n- [`31_iterative_refinement.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/31_iterative_refinement.py)\n- [`32_configurable_security_policy.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/32_configurable_security_policy.py)\n- [`33_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/33_hooks)\n- [`34_critic_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/34_critic_example.py)\n- [`35_subscription_login.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/35_subscription_login.py)\n- [`36_event_json_to_openai_messages.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/36_event_json_to_openai_messages.py)\n- [`37_llm_profile_store`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/37_llm_profile_store)\n- [`38_browser_session_recording.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/38_browser_session_recording.py)\n- [`39_llm_fallback.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/39_llm_fallback.py)\n- [`40_acp_agent_example.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/40_acp_agent_example.py)\n- [`41_task_tool_set.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/41_task_tool_set.py)\n- [`42_file_based_subagents.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/42_file_based_subagents.py)\n- [`43_mixed_marketplace_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/43_mixed_marketplace_skills)\n- [`44_model_switching_in_convo.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/44_model_switching_in_convo.py)\n- [`45_parallel_tool_execution.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/45_parallel_tool_execution.py)\n- [`46_agent_settings.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/46_agent_settings.py)\n- [`47_defense_in_depth_security.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/47_defense_in_depth_security.py)\n- [`48_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/48_conversation_fork.py)\n- [`49_switch_llm_tool.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/49_switch_llm_tool.py)\n- [`50_async_cancellation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/50_async_cancellation.py)\n- [`51_agent_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/51_agent_hooks)\n- [`52_dynamic_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/52_dynamic_workflow.py)\n- [`53_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/53_client_defined_tools.py)\n\n### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)\n\n- [`01_convo_with_local_agent_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/01_convo_with_local_agent_server.py)\n- [`02_convo_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/02_convo_with_docker_sandboxed_server.py)\n- [`03_browser_use_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/03_browser_use_with_docker_sandboxed_server.py)\n- [`04_convo_with_api_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/04_convo_with_api_sandboxed_server.py)\n- [`05_vscode_with_docker_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/05_vscode_with_docker_sandboxed_server.py)\n- [`06_custom_tool`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/06_custom_tool)\n- [`07_convo_with_cloud_workspace.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/07_convo_with_cloud_workspace.py)\n- [`08_convo_with_apptainer_sandboxed_server.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/08_convo_with_apptainer_sandboxed_server.py)\n- [`09_acp_agent_with_remote_runtime.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/09_acp_agent_with_remote_runtime.py)\n- [`10_cloud_workspace_share_credentials.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/10_cloud_workspace_share_credentials.py)\n- [`11_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/11_conversation_fork.py)\n- [`12_settings_and_secrets_api.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/12_settings_and_secrets_api.py)\n- [`13_workspace_get_llm.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/13_workspace_get_llm.py)\n- [`14_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/14_client_defined_tools.py)\n- [`15_openai_compatible_gateway.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/15_openai_compatible_gateway.py)\n- [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)\n- [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)\n\n### [`03_github_workflows/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows)\n\n- [`01_basic_action`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/01_basic_action)\n- [`02_pr_review`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/02_pr_review)\n- [`03_todo_management`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/03_todo_management)\n- [`04_datadog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/04_datadog_debugging)\n- [`05_posthog_debugging`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/03_github_workflows/05_posthog_debugging)\n\n### [`04_llm_specific_tools/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/04_llm_specific_tools)\n\n- [`01_gpt5_apply_patch_preset.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/01_gpt5_apply_patch_preset.py)\n- [`02_gemini_file_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/04_llm_specific_tools/02_gemini_file_tools.py)\n\n### [`05_skills_and_plugins/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins)\n\n- [`01_loading_agentskills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/01_loading_agentskills)\n- [`02_loading_plugins`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/02_loading_plugins)\n- [`03_managing_installed_skills`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/05_skills_and_plugins/03_managing_installed_skills)"
331
331
  },
332
332
  {
333
333
  "name": "pdflatex",
@@ -190,6 +190,7 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
190
190
  - [`50_async_cancellation.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/50_async_cancellation.py)
191
191
  - [`51_agent_hooks`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/01_standalone_sdk/51_agent_hooks)
192
192
  - [`52_dynamic_workflow.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/52_dynamic_workflow.py)
193
+ - [`53_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/53_client_defined_tools.py)
193
194
 
194
195
  ### [`02_remote_agent_server/`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server)
195
196
 
@@ -206,6 +207,8 @@ Source: [`examples/`](https://github.com/OpenHands/software-agent-sdk/tree/main/
206
207
  - [`11_conversation_fork.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/11_conversation_fork.py)
207
208
  - [`12_settings_and_secrets_api.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/12_settings_and_secrets_api.py)
208
209
  - [`13_workspace_get_llm.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/13_workspace_get_llm.py)
210
+ - [`14_client_defined_tools.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/14_client_defined_tools.py)
211
+ - [`15_openai_compatible_gateway.py`](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/02_remote_agent_server/15_openai_compatible_gateway.py)
209
212
  - [`hook_scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/hook_scripts)
210
213
  - [`scripts`](https://github.com/OpenHands/software-agent-sdk/tree/main/examples/02_remote_agent_server/scripts)
211
214