@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.
- package/.release-please-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/github-pr-reviewer/SKILL.md +183 -40
- package/skills/github-pr-reviewer/references/state-schema.md +98 -0
- package/skills/github-pr-reviewer/scripts/main.py +625 -0
- package/skills/index.js +2 -2
- package/skills/openhands-sdk/SKILL.md +3 -0
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
@@ -11,79 +11,222 @@ triggers:
|
|
|
11
11
|
|
|
12
12
|
# GitHub PR Reviewer Automation
|
|
13
13
|
|
|
14
|
-
|
|
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
|
|
25
|
+
### Required secret
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
Verify that the following secret is set in **OpenHands Settings → Secrets**:
|
|
23
28
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
+
Fix any syntax errors before proceeding.
|
|
48
130
|
|
|
49
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
138
|
+
```bash
|
|
139
|
+
tar -czf /tmp/pr-reviewer.tar.gz -C /tmp/pr-reviewer-build .
|
|
56
140
|
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
60
|
-
-
|
|
61
|
-
-
|
|
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
|
-
|
|
148
|
+
echo "Uploaded: $TARBALL_PATH"
|
|
149
|
+
```
|
|
64
150
|
|
|
65
|
-
|
|
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
|
|
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
|
-
"
|
|
77
|
-
"
|
|
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
|
-
|
|
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
|
-
> -
|
|
88
|
-
> -
|
|
89
|
-
> -
|
|
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\
|
|
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
|
|