openhands-api · diff
git:20260225.a20257e to git:20260322.fdd6c73
352 added, 48 removed. Audit A to A.
---
name: openhands-api
- description: Use the OpenHands REST API (V0) to create and manage agent conversations programmatically. Includes minimal Python and TypeScript clients under scripts/.
+ description: Reference skill for the OpenHands Cloud REST API (V1), including how to start additional cloud conversations for fresh-context or delegated work. Use when you need to automate common OpenHands Cloud actions; don't use for general sandbox/dev tasks unrelated to the OpenHands API.
triggers:
- - openhands-api-v0
- - openhands-cloud-v0
- - oh-cloud-v0
+ - openhands-api
+ - openhands-api-v1
+ - openhands-cloud
+ - openhands-cloud-api-v1
+ - oh-api-v1
+ - oh-cloud-api-v1
---
- This skill provides a minimal, easy-to-copy OpenHands API client for starting and monitoring conversations.
+ This skill documents the **OpenHands Cloud API** (V1) and provides small, easy-to-copy clients.
- It is intentionally small and focused:
+ It is intentionally focused on common OpenHands Cloud workflows:
- Defaults to OpenHands Cloud (`https://app.all-hands.dev`).
- - Targets the **legacy V0 REST API routes** implemented in `OpenHands/OpenHands` under `openhands/server/routes/`.
- - Implements only a few core endpoints so an AI agent can quickly adapt/extend it.
+ - Targets the **V1 app server REST API** under `/api/v1/...`.
+ - Includes a few **agent server** endpoints (inside a sandbox) that use `X-Session-API-Key`.
+ - Covers the **multi-conversation delegation pattern**: start separate Cloud conversations when you want fresh context windows or background work.
- ## Quick start (Python)
+ ## When to use this skill
+ Use this skill when you need to:
+
+ - start or inspect OpenHands Cloud conversations from code
+ - monitor async startup via start-task polling
+ - monitor execution status for long-running jobs
+ - create separate Cloud conversations for parallel or background work
+ - access sandbox agent-server endpoints once a conversation is running
+
+ ## Auth
+
+ ### App server (Cloud)
+
+ Use Bearer auth:
+
+ - Header: `Authorization: Bearer <OPENHANDS_CLOUD_API_KEY>`
+ - Preferred env var: `OPENHANDS_CLOUD_API_KEY`
+ - Backward-compatible env var: `OPENHANDS_API_KEY`
+
+ ### Agent server (inside a sandbox)
+
+ Use session auth:
+
+ - Header: `X-Session-API-Key: <session_api_key>`
+
+ How to obtain `agent_server_url` and `session_api_key`:
+
+ 1. Start or fetch an app conversation via the app server (Bearer auth), e.g.:
+ - `POST /api/v1/app-conversations`
+ - or `GET /api/v1/app-conversations?ids=<conversation_id>`
+ 2. In the returned JSON, look for sandbox/runtime connection fields (names vary slightly by deployment/version). Common patterns:
+ - a sandbox object containing `agent_server_url` (or similar)
+ - a session key such as `session_api_key` (or similar)
+ 3. Use those values to call the agent server directly:
+ - Base: `{agent_server_url}/api/...`
+ - Header: `X-Session-API-Key: <session_api_key>`
+
+ Example (common field names; adjust to your deployment):
+
```python
- from scripts.openhands_api import OpenHandsAPI
+ # using the minimal Python client (`OpenHandsAPI`)
+ conv = api.app_conversation_get(app_conversation_id)
- api = OpenHandsAPI(api_key="...") # or set OPENHANDS_API_KEY
+ session_api_key = conv.get("session_api_key")
+ conversation_url = conv.get("conversation_url", "")
- conv = api.create_conversation(
- initial_user_msg="Please audit this repo for flaky tests and open a PR fixing them.",
- repository="owner/repo",
+ # `conversation_url` often looks like: https://<runtime-host>/api/conversations/<id>
+ agent_server_url = conversation_url.rsplit("/api/conversations", 1)[0]
+ ```
+
+
+ If those fields are not present on the conversation record, list/search sandboxes (`GET /api/v1/sandboxes/search`) and use the sandbox referenced by the conversation to locate the agent server URL + session key.
+
+ ## Common V1 app server endpoints
+
+ The following are the main endpoints implemented in the minimal client:
+
+ - `GET /api/v1/users/me` — validate auth and inspect current account
+ - `GET /api/v1/app-conversations/search?limit=...` — list recent conversations
+ - `GET /api/v1/app-conversations?ids=...` — fetch conversation records by id (batch)
+ - `GET /api/v1/app-conversations/count` — count conversations
+ - `POST /api/v1/app-conversations` — start a new conversation (creates a sandbox)
+ - `GET /api/v1/app-conversations/start-tasks?ids=...` — check async start-task status
+ - `GET /api/v1/conversation/{app_conversation_id}/events/search?limit=...` — read conversation events
+ - `GET /api/v1/conversation/{app_conversation_id}/events/count` — count events
+ - `GET /api/v1/sandboxes/search?limit=...` — list sandboxes
+ - `POST /api/v1/sandboxes/{sandbox_id}/pause` / `.../resume` — manage sandbox lifecycle
+ - `GET /api/v1/app-conversations/{app_conversation_id}/download` — download trajectory zip
+
+ ## Delegating work with additional Cloud conversations
+
+ Use the Cloud API when you want a **separate OpenHands conversation** with its own fresh context window.
+ This is useful for:
+
+ - background jobs that can run independently
+ - parallel investigations or implementation tasks
+ - long-running work where you want to keep the current conversation focused
+ - task-specific contexts, such as one conversation building a component while another runs tests
+
+ ### Delegation checklist
+
+ When you start a delegated Cloud conversation:
+
+ 1. Write a **self-contained task description**. Do not assume the new conversation has any context from the current one.
+ 2. Include the **repository**, branch, relevant file paths, constraints, and expected output.
+ 3. Start the new conversation with `POST /api/v1/app-conversations`.
+ 4. Poll the start-task until `status` is `READY` and you have an `app_conversation_id`.
+ 5. Monitor the delegated conversation via `GET /api/v1/app-conversations?ids=...`.
+ 6. Share or store the Cloud URL: `https://app.all-hands.dev/conversations/<app_conversation_id>`.
+
+ ### Minimal cURL flow
+
+ ```bash
+ curl -X POST "https://app.all-hands.dev/api/v1/app-conversations" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "initial_message": {
+ "content": [{"type": "text", "text": "Investigate flaky tests in tests/test_api.py. Report the root cause and propose a fix."}]
+ },
+ "selected_repository": "owner/repo"
+ }'
+ ```
+
+ If the response does not already include `app_conversation_id`, poll the start-task:
+
+ ```bash
+ curl -s "https://app.all-hands.dev/api/v1/app-conversations/start-tasks?ids=${START_TASK_ID}" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
+ ```
+
+ Then check execution status:
+
+ ```bash
+ curl -s "https://app.all-hands.dev/api/v1/app-conversations?ids=${APP_CONVERSATION_ID}" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY}"
+ ```
+
+ ### Minimal Python flow
+
+ ```python
+ from openhands_api import OpenHandsAPI
+
+ api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
+
+ start = api.app_conversation_start(
+ initial_message=(
+ "Implement the requested dashboard component in src/dashboard.tsx. "
+ "Update any related tests and summarize the changes."
+ ),
+ selected_repository="owner/repo",
selected_branch="main",
+ title="Dashboard component task",
)
- conversation_id = conv["conversation_id"]
- final = api.poll_until_terminal(conversation_id, timeout_s=1800, poll_interval_s=30)
- print(final["status"], final.get("url"))
+ ready = start
+ if not ready.get("app_conversation_id"):
+ ready = api.poll_start_task_until_ready(start["id"])
+
+ conversation_id = ready["app_conversation_id"]
+ print(f"Delegated conversation: {api.base_url}/conversations/{conversation_id}")
+
+ status = api.app_conversation_get(conversation_id)
+ print(status.get("sandbox_status"), status.get("execution_status"))
+
+ api.close()
```
- ## Quick start (TypeScript)
+ ### Parallelism guidance
- ```ts
- import { OpenHandsAPI } from "./scripts/openhands_api";
+ - Prefer **5 or fewer** concurrently running delegated conversations.
+ - Before starting more, check recent conversations and count how many are still `execution_status == "running"`.
+ - Batch specific conversation lookups with `GET /api/v1/app-conversations?ids=...` when you already know their ids.
- const api = new OpenHandsAPI({ apiKey: process.env.OPENHANDS_API_KEY! });
+ Example:
- const conv = await api.createConversation({
- initialUserMsg: "Run dependency upgrades and open a PR.",
- repository: "owner/repo",
- selectedBranch: "main",
- });
+ ```python
+ items = api.app_conversations_search(limit=50).get("items", [])
+ running = [item for item in items if item.get("execution_status") == "running"]
+ if len(running) >= 5:
+ print("Wait for some delegated conversations to finish before starting more.")
+ ```
- const final = await api.pollUntilTerminal(conv.conversation_id, {
- timeoutMs: 30 * 60_000,
- pollIntervalMs: 30_000,
- });
- console.log(final.status, final.url);
+ ### Start-task vs `app_conversation_id` (common pitfall)
+
+ In many deployments, `POST /api/v1/app-conversations` is **asynchronous** and returns a **start-task** object:
+
+ - `id` is the **start_task_id**
+ - `app_conversation_id` is the id you should use for conversation operations like:
+ - `GET /api/v1/app-conversations/{app_conversation_id}/download`
+ - `GET /api/v1/conversation/{app_conversation_id}/events/...`
+
+ If `app_conversation_id` is not present in the initial response, fetch it via:
+
+ - `GET /api/v1/app-conversations/start-tasks?ids=<start_task_id>`
+
+ If you pass a **start_task_id** to `/download`, you will get `404 Not Found`.
+
+ ## Common agent server endpoints
+
+ These run against `agent_server_url` (not the app server):
+
+ - `POST {agent_server_url}/api/bash/execute_bash_command`
+ - `GET {agent_server_url}/api/file/download/<absolute_path>`
+ - `POST {agent_server_url}/api/file/upload/<absolute_path>` (multipart)
+ - `GET {agent_server_url}/api/conversations/{conversation_id}/events/search`
+ - `GET {agent_server_url}/api/conversations/{conversation_id}/events/count`
+
+ ### Counting events (recommended approach)
+
+ If you need to know how many events a conversation has, you can:
+
+ 1. **App server count (fastest when working)**
+ - `GET /api/v1/conversation/{app_conversation_id}/events/count`
+ 2. **Agent server count (reliable fallback)**
+ - `GET {agent_server_url}/api/conversations/{app_conversation_id}/events/count`
+ 3. **Trajectory zip fallback (heavier, but still one call + gives full payloads)**
+ - `GET /api/v1/app-conversations/{app_conversation_id}/download`
+ - Unzip and count `event_*.json` files
+
+ Do **not** rely on the last event `id` to infer the total number of events.
+ In the agent-server API, event IDs are UUIDs (not monotonically increasing integers).
+
+ ## Troubleshooting
+
+ For common issues and solutions, see [TROUBLESHOOTING.md](references/TROUBLESHOOTING.md).
+
+ ## Event structure (for debugging)
+
+ Events returned by:
+
+ - app server: `GET /api/v1/conversation/{id}/events/search`
+ - agent server: `GET {agent_server_url}/api/conversations/{id}/events/search`
+
+ …share the same high-level shape.
+
+ Each event typically includes:
+
+ - `id` (UUID)
+ - `timestamp`
+ - `kind`
+ - `source`
+
+ Common `kind` values:
+
+ | kind | source (typical) | key fields (common) | purpose |
+ |---|---|---|---|
+ | `ActionEvent` | `agent` | `tool_name`, `tool_call_id`, `action` | tool call requested by the agent |
+ | `ObservationEvent` | `environment` | `tool_name`, `tool_call_id`, `action_id`, `observation` | tool result produced by the sandbox/environment |
+ | `MessageEvent` | `user` / `assistant` | `message` (or similar) | user/assistant chat messages |
+ | `ConversationStateUpdateEvent` | `environment` | `key`, `value` | state transitions/metadata |
+
+ Linking tool calls:
+
+ - `ActionEvent.tool_call_id` == `ObservationEvent.tool_call_id`
+ - `ObservationEvent.action_id` == `ActionEvent.id`
+
+ Example (simplified):
+
+ ```json
+ {
+ "id": "<action-event-uuid>",
+ "kind": "ActionEvent",
+ "source": "agent",
+ "tool_name": "terminal",
+ "tool_call_id": "toolu_...",
+ "action": {"command": "ls"}
+ }
```
- ## Example (modeled after enyst/playground#105)
+ ```json
+ {
+ "id": "<observation-event-uuid>",
+ "kind": "ObservationEvent",
+ "source": "environment",
+ "tool_name": "terminal",
+ "tool_call_id": "toolu_...",
+ "action_id": "<action-event-uuid>",
+ "observation": {"exit_code": 0, "stdout": "..."}
+ }
+ ```
- The following example shows a simple automation pattern:
+ ## Debugging one-liners (events)
- 1. Start a conversation with a prompt template
- 2. Print the conversation URL for humans
- 3. Optionally poll until completion
+ These assume you're querying the **app server** endpoint. For agent-server queries, swap the URL base + use `X-Session-API-Key`.
+ ### Print a quick timeline
+
```bash
- export OPENHANDS_API_KEY="..."
- python skills/openhands_api/scripts/openhands_api.py \
- new-conversation \
- --prompt-file skills/openhands_api/references/example_prompt.md \
+ curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=100" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
+ -H "Accept: application/json" | \
+ python3 - <<'PY'
+ import json, sys
+ items = (json.load(sys.stdin) or {}).get("items", [])
+ for i, e in enumerate(items):
+ print(f"{i:04d} {e.get('timestamp','')} {e.get('source','')} {e.get('kind','')}")
+ PY
+ ```
+
+ ### Find error-like events
+
+ ```bash
+ curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
+ -H "Accept: application/json" | \
+ python3 - <<'PY'
+ import json, sys
+ items = (json.load(sys.stdin) or {}).get("items", [])
+ for i, e in enumerate(items):
+ if e.get("kind") == "ErrorEvent" or ("code" in e and "detail" in e):
+ print(i, e.get("kind"), e.get("code"), str(e.get("detail", ""))[:400])
+ PY
+ ```
+
+ ### Check tool-call matching (unmatched actions / duplicate observations)
+
+ ```bash
+ curl -s "${BASE_URL:-https://app.all-hands.dev}/api/v1/conversation/${APP_CONVERSATION_ID}/events/search?limit=200" \
+ -H "Authorization: Bearer ${OPENHANDS_CLOUD_API_KEY:-$OPENHANDS_API_KEY}" \
+ -H "Accept: application/json" | \
+ python3 - <<'PY'
+ import json, sys
+ from collections import Counter
+ items = (json.load(sys.stdin) or {}).get("items", [])
+ action_ids = {e.get("id") for e in items if e.get("kind") == "ActionEvent"}
+ obs_action_ids = [e.get("action_id") for e in items if e.get("kind") == "ObservationEvent" and e.get("action_id")]
+ observed = set(obs_action_ids)
+ print("actions:", len(action_ids))
+ print("observations:", len(observed))
+ unmatched = action_ids - observed
+ print("unmatched actions:", list(unmatched)[:20] if unmatched else "none")
+ dups = [aid for aid, c in Counter(obs_action_ids).items() if c > 1]
+ print("duplicate observation action_ids:", list(dups)[:20] if dups else "none")
+ PY
+ ```
+
+
+ ## Quick start (Python)
+
+ ```python
+ # Copy `skills/openhands-api/scripts/openhands_api.py` into your project (e.g. as `openhands_api.py`),
+ # then import it normally:
+ from openhands_api import OpenHandsAPI
+
+ api = OpenHandsAPI() # prefers OPENHANDS_CLOUD_API_KEY
+
+ me = api.users_me()
+ print(me)
+
+ recent = api.app_conversations_search(limit=5)
+ print(recent)
+
+ api.close()
+ ```
+
+ ## CLI examples
+
+ Search conversations:
+
+ ```bash
+ export OPENHANDS_CLOUD_API_KEY="..."
+ python skills/openhands-api/scripts/openhands_api.py search-conversations --limit 5
+ ```
+
+ Start a conversation from a prompt file:
+
+ ```bash
+ python skills/openhands-api/scripts/openhands_api.py start-conversation \
+ --prompt-file skills/openhands-api/references/example_prompt.md \
--repo owner/repo \
- --branch main \
- --poll
+ --branch main
```
## Notes for AI agents extending this client
- - The server expects a Bearer token (`Authorization: Bearer <OPENHANDS_API_KEY>`).
- - The most important endpoint is `POST /api/conversations`.
- - If you need streaming/logs, the V0 server exposes `GET /api/conversations/{conversation_id}/events`.
- - If you need the full event history in one call, use `GET /api/conversations/{conversation_id}/trajectory`.
+ - Prefer `.../search` endpoints with a small `limit`.
+ - Avoid loops that could generate many API calls.
+ - Start conversations only when asked: it may create sandboxes and cost money.
+ - For sandbox file operations and command execution, use the agent server endpoints with `X-Session-API-Key`.
See also:
- - `skills/openhands_api/references/README.md` (API docs pointers)
- - `skills/openhands_api/scripts/openhands_api.py` and `skills/openhands_api/scripts/openhands_api.ts`
+ - `skills/openhands-api/scripts/openhands_api.py`
+ - The original inspiration client: `enyst/llm-playground` → `openhands-api-client-v1/scripts/cloud_api_v1.py`
+ - Troubleshooting content and real-world usage feedback → `https://github.com/jpshackelford/.openhands/tree/main/skills/openhands-cloud-api`