Compare commits

..

16 Commits

Author SHA1 Message Date
openhands e136651e46 Add detailed debug logs to remote runtime implementation 2025-04-30 16:17:26 +00:00
Robert Brennan 760a14482e Fix ValueError when latest_event_id is undefined (#8168)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Graham Neubig <neubig@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 12:50:44 +00:00
Ryan H. Tran b5338c69d6 Upgrade openhands-aci to 0.2.11 (#8154) 2025-04-30 02:54:15 +00:00
Hiroki Miyaji c99f031cdb docs: fix broken links (#8169) 2025-04-29 22:31:48 -04:00
mamoodi bcc28a12fe Release 0.35.0 (#8131)
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Chuck Butkus <chuck@all-hands.dev>
2025-04-29 17:45:40 -04:00
Dani c82b3378a6 Fix issue #8145: Correct name for max_tokens for condenser in config.template.toml (#8165) 2025-04-29 20:28:01 +00:00
Ray Myers a6d3db3ce7 Update anyio to 4.9.0 (#8161)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-29 20:05:57 +00:00
SDGLBL 4cbbfd799c fix(memory): Fix empty string content handling in ConversationMemory (#8148)
Co-authored-by: lijie.20 <lijie.20@bytedance.com>
2025-04-29 21:03:13 +02:00
Xingyao Wang 0b728c0c79 [agent]: update system message to prevent the agent being too obsessed with setting up environment (#8007) 2025-04-30 00:10:44 +08:00
Ryosuke Hayashi e35c8ee173 fix OpenAPI schema generation error caused by mappingproxy in models (#8121) 2025-04-29 16:05:02 +00:00
Xingyao Wang 9a9b143620 nit: improve error message when action is not executed (#7029)
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-30 00:04:11 +08:00
sp.wack 38578bd5f5 hotifx(frontend): Critical fix for black screen (#8158) 2025-04-29 15:56:25 +00:00
dependabot[bot] 7b2c88ae6b chore(deps): bump the version-all group with 4 updates (#8157)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-29 17:46:20 +02:00
dependabot[bot] 0cbf3987f8 chore(deps): bump the version-all group in /frontend with 9 updates (#8155)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
2025-04-29 15:32:27 +00:00
chuckbutkus d18edc8b30 Move Terms of Service acceptance to dedicated page (#8071)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Robert Brennan <accounts@rbren.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: amanape <83104063+amanape@users.noreply.github.com>
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: tofarr <tofarr@gmail.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
Co-authored-by: மனோஜ்குமார் பழனிச்சாமி <smartmanoj42857@gmail.com>
Co-authored-by: Lenshood <lenshood.zxh@gmail.com>
Co-authored-by: OpenHands <opendevin@all-hands.dev>
Co-authored-by: mamoodi <mamoodiha@gmail.com>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-04-29 15:12:45 +00:00
Graham Neubig 42eb355a68 Fix OpenRouter context window exceeded error detection (#8150)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-29 10:05:56 -04:00
58 changed files with 2253 additions and 2475 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker container image by
setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.34-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.35-nikolaik`
## Develop inside Docker container
+3 -3
View File
@@ -52,17 +52,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -391,7 +391,7 @@ type = "noop"
#[llm.condenser]
#model = "gpt-4o"
#temperature = 0.1
#max_tokens = 1024
#max_input_tokens = 1024
#################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation
+1 -1
View File
@@ -11,7 +11,7 @@ services:
- BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.34-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.35-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of openhands-state for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -13,16 +13,16 @@
La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
Vous pouvez également exécuter OpenHands en mode [headless scriptable](https://docs.all-hands.dev/modules/usage/how-to/headless-mode), en tant que [CLI interactive](https://docs.all-hands.dev/modules/usage/how-to/cli-mode), ou en utilisant l'[Action GitHub OpenHands](https://docs.all-hands.dev/modules/usage/how-to/github-action).
@@ -13,7 +13,7 @@ C'est le Runtime par défaut qui est utilisé lorsque vous démarrez OpenHands.
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
@@ -13,7 +13,7 @@ OpenHandsがリポジトリで動作する際:
1. リポジトリに`.openhands/microagents/`が存在する場合、そこからリポジトリ固有の指示を読み込みます。
2. 会話のキーワードによってトリガーされる一般的なガイドラインを読み込みます。
現在の[パブリックMicroagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)を参照してください。
現在の[パブリックMicroagents](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)を参照してください。
## Microagentのフォーマット
@@ -88,4 +88,4 @@ triggers:
- ビルド時間とイメージサイズを最適化
```
より多くの例については、[現在のパブリックマイクロエージェント](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)をご覧ください。
より多くの例については、[現在のパブリックマイクロエージェント](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)をご覧ください。
@@ -25,7 +25,7 @@ nikolaik の `SANDBOX_RUNTIME_CONTAINER_IMAGE` は、ランタイムサーバー
```bash
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \
@@ -82,5 +82,5 @@ docker network create openhands-network
# 分離されたネットワークで OpenHands を実行
docker run # ... \
--network openhands-network \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
@@ -35,7 +35,7 @@ Para executar o OpenHands no modo CLI com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "escreva um script bash que imprima oi"
```
@@ -58,17 +58,17 @@
A maneira mais fácil de executar o OpenHands é no Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
Você encontrará o OpenHands em execução em http://localhost:3000!
@@ -13,7 +13,7 @@ Quando o OpenHands trabalha com um repositório, ele:
1. Carrega instruções específicas do repositório de `.openhands/microagents/`, se presentes no repositório.
2. Carrega diretrizes gerais acionadas por palavras-chave nas conversas.
Veja os [Microagentes Públicos](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) atuais.
Veja os [Microagentes Públicos](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) atuais.
## Formato do Microagente
@@ -4,7 +4,7 @@
Microagentes públicos são diretrizes especializadas acionadas por palavras-chave para todos os usuários do OpenHands.
Eles são definidos em arquivos markdown no diretório
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge).
[`microagents/`](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents).
Microagentes públicos:
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento.
@@ -149,5 +149,5 @@ Lembre-se de:
- Otimizar para tempo de build e tamanho da imagem
```
Veja os [microagentes públicos atuais](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge) para
Veja os [microagentes públicos atuais](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents) para
mais exemplos.
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
```
@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
你也可以在可脚本化的[无头模式](https://docs.all-hands.dev/modules/usage/how-to/headless-mode)下运行 OpenHands,作为[交互式 CLI](https://docs.all-hands.dev/modules/usage/how-to/cli-mode),或使用 [OpenHands GitHub Action](https://docs.all-hands.dev/modules/usage/how-to/github-action)。
@@ -11,7 +11,7 @@
```
docker run # ...
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-v /var/run/docker.sock:/var/run/docker.sock \
# ...
```
+2 -2
View File
@@ -35,7 +35,7 @@ To run OpenHands in CLI mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.cli
```
@@ -1 +0,0 @@
# Using GitLab CI Runners
+2 -2
View File
@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.34 \
docker.all-hands.dev/all-hands-ai/openhands:0.35 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
+3 -3
View File
@@ -58,17 +58,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
The easiest way to run OpenHands is in Docker.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.34-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.35-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.34
docker.all-hands.dev/all-hands-ai/openhands:0.35
```
You'll find OpenHands running at http://localhost:3000!
@@ -46,4 +46,4 @@ Keyword-triggered microagents:
- Apply their specialized knowledge and capabilities.
- Follow defined guidelines and restrictions.
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents/knowledge)
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
@@ -1,12 +1,16 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeAll, afterAll } from "vitest";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import { AuthModal } from "#/components/features/waitlist/auth-modal";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as AuthHook from "#/context/auth-context";
// Mock the useAuthUrl hook
vi.mock("#/hooks/use-auth-url", () => ({
useAuthUrl: () => "https://gitlab.com/oauth/authorize"
}));
describe("AuthModal", () => {
beforeAll(() => {
beforeEach(() => {
vi.stubGlobal("location", { href: "" });
vi.spyOn(AuthHook, "useAuth").mockReturnValue({
providersAreSet: false,
@@ -16,50 +20,29 @@ describe("AuthModal", () => {
});
});
afterAll(() => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
vi.resetAllMocks();
});
it("should render a tos checkbox that is unchecked by default", () => {
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
expect(checkbox).not.toBeChecked();
});
it("should only enable the identity provider buttons if the tos checkbox is checked", async () => {
const user = userEvent.setup();
render(<AuthModal githubAuthUrl={null} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
it("should render the GitHub and GitLab buttons", () => {
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
expect(githubButton).toBeDisabled();
expect(gitlabButton).toBeDisabled();
await user.click(checkbox);
expect(githubButton).not.toBeDisabled();
expect(gitlabButton).not.toBeDisabled();
expect(githubButton).toBeInTheDocument();
expect(gitlabButton).toBeInTheDocument();
});
it("should set user analytics consent to true when the user checks the tos checkbox", async () => {
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
const user = userEvent.setup();
render(<AuthModal githubAuthUrl="mock-url" appMode="saas" />);
const mockUrl = "https://github.com/login/oauth/authorize";
render(<AuthModal githubAuthUrl={mockUrl} appMode="saas" />);
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(githubButton);
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(window.location.href).toBe(mockUrl);
});
});
@@ -0,0 +1,136 @@
import { render, screen } from "@testing-library/react";
import { it, describe, expect, vi, beforeEach, afterEach } from "vitest";
import userEvent from "@testing-library/user-event";
import AcceptTOS from "#/routes/accept-tos";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as ToastHandlers from "#/utils/custom-toast-handlers";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { openHands } from "#/api/open-hands-axios";
// Mock the react-router hooks
vi.mock("react-router", () => ({
useNavigate: () => vi.fn(),
useSearchParams: () => [
{
get: (param: string) => {
if (param === "redirect_url") {
return "/dashboard";
}
return null;
},
},
],
}));
// Mock the axios instance
vi.mock("#/api/open-hands-axios", () => ({
openHands: {
post: vi.fn(),
},
}));
// Mock the toast handlers
vi.mock("#/utils/custom-toast-handlers", () => ({
displayErrorToast: vi.fn(),
}));
// Create a wrapper with QueryClientProvider
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("AcceptTOS", () => {
beforeEach(() => {
vi.stubGlobal("location", { href: "" });
});
afterEach(() => {
vi.unstubAllGlobals();
vi.resetAllMocks();
});
it("should render a TOS checkbox that is unchecked by default", () => {
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
expect(checkbox).not.toBeChecked();
expect(continueButton).toBeDisabled();
});
it("should enable the continue button when the TOS checkbox is checked", async () => {
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
expect(continueButton).toBeDisabled();
await user.click(checkbox);
expect(continueButton).not.toBeDisabled();
});
it("should set user analytics consent to true when the user accepts TOS", async () => {
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
// Mock the API response
vi.mocked(openHands.post).mockResolvedValue({
data: { redirect_url: "/dashboard" },
});
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
await user.click(continueButton);
// Wait for the mutation to complete
await new Promise(process.nextTick);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
expect(openHands.post).toHaveBeenCalledWith("/api/accept_tos", {
redirect_url: "/dashboard",
});
});
it("should handle external redirect URLs", async () => {
// Mock the API response with an external URL
const externalUrl = "https://example.com/callback";
vi.mocked(openHands.post).mockResolvedValue({
data: { redirect_url: externalUrl },
});
const user = userEvent.setup();
render(<AcceptTOS />, { wrapper: createWrapper() });
const checkbox = screen.getByRole("checkbox");
await user.click(checkbox);
const continueButton = screen.getByRole("button", { name: "TOS$CONTINUE" });
await user.click(continueButton);
// Wait for the mutation to complete
await new Promise(process.nextTick);
expect(window.location.href).toBe(externalUrl);
});
});
+1421 -2202
View File
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,22 +1,22 @@
{
"name": "openhands-frontend",
"version": "0.34.0",
"version": "0.35.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "2.7.6",
"@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.1",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.5.2",
"@react-router/serve": "^7.5.2",
"@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.3",
"@react-types/shared": "^3.29.0",
"@reduxjs/toolkit": "^2.7.0",
"@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.0",
"@tanstack/react-query": "^5.74.7",
"@tanstack/react-query": "^5.74.9",
"@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
@@ -24,14 +24,14 @@
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.9.2",
"i18next": "^25.0.1",
"i18next": "^25.0.2",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.27",
"jose": "^6.0.10",
"lucide-react": "^0.503.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.236.7",
"posthog-js": "^1.237.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -40,7 +40,7 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.5.2",
"react-router": "^7.5.3",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
@@ -82,7 +82,7 @@
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.52.0",
"@react-router/dev": "^7.5.2",
"@react-router/dev": "^7.5.3",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/dom": "^10.4.0",
@@ -4,8 +4,6 @@ import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body";
import { TOSCheckbox } from "./tos-checkbox";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
@@ -19,7 +17,6 @@ interface AuthModalProps {
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const { t } = useTranslation();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const gitlabAuthUrl = useAuthUrl({
appMode: appMode || null,
@@ -28,14 +25,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => {
if (githubAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl;
}
};
const handleGitLabAuth = () => {
if (gitlabAuthUrl) {
handleCaptureConsent(true);
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl;
}
};
@@ -50,11 +47,8 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</h1>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<div className="flex flex-col gap-3 w-full">
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitHubAuth}
@@ -65,7 +59,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</BrandButton>
<BrandButton
isDisabled={!isTosAccepted}
type="button"
variant="primary"
onClick={handleGitLabAuth}
+19 -5
View File
@@ -13,21 +13,35 @@ import posthog from "posthog-js";
import "./i18n";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import store from "./store";
import { useConfig } from "./hooks/query/use-config";
import { AuthProvider } from "./context/auth-context";
import { queryClientConfig } from "./query-client-config";
import OpenHands from "./api/open-hands";
import { displayErrorToast } from "./utils/custom-toast-handlers";
function PosthogInit() {
const { data: config } = useConfig();
const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
null,
);
React.useEffect(() => {
if (config?.POSTHOG_CLIENT_KEY) {
posthog.init(config.POSTHOG_CLIENT_KEY, {
(async () => {
try {
const config = await OpenHands.getConfig();
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
} catch (error) {
displayErrorToast("Error fetching PostHog client key");
}
})();
}, []);
React.useEffect(() => {
if (posthogClientKey) {
posthog.init(posthogClientKey, {
api_host: "https://us.i.posthog.com",
person_profiles: "identified_only",
});
}
}, [config]);
}, [posthogClientKey]);
return null;
}
+5 -1
View File
@@ -1,14 +1,18 @@
import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useBalance = () => {
const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
return useQuery({
queryKey: ["user", "balance"],
queryFn: OpenHands.getBalance,
enabled:
config?.APP_MODE === "saas" && config?.FEATURE_FLAGS.ENABLE_BILLING,
!isOnTosPage &&
config?.APP_MODE === "saas" &&
config?.FEATURE_FLAGS.ENABLE_BILLING,
});
};
+8 -3
View File
@@ -1,10 +1,15 @@
import { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useConfig = () =>
useQuery({
export const useConfig = () => {
const isOnTosPage = useIsOnTosPage();
return useQuery({
queryKey: ["config"],
queryFn: OpenHands.getConfig,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
gcTime: 1000 * 60 * 15, // 15 minutes,
enabled: !isOnTosPage,
});
};
+3 -1
View File
@@ -3,17 +3,19 @@ import React from "react";
import OpenHands from "#/api/open-hands";
import { useConfig } from "./use-config";
import { useAuth } from "#/context/auth-context";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useIsAuthed = () => {
const { providersAreSet } = useAuth();
const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
const appMode = React.useMemo(() => config?.APP_MODE, [config]);
return useQuery({
queryKey: ["user", "authenticated", providersAreSet, appMode],
queryFn: () => OpenHands.authenticate(appMode!),
enabled: !!appMode,
enabled: !!appMode && !isOnTosPage,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
retry: false,
+4
View File
@@ -4,6 +4,7 @@ import posthog from "posthog-js";
import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { Settings } from "#/types/settings";
const getSettingsQueryFn = async (): Promise<Settings> => {
@@ -31,6 +32,8 @@ export const useSettings = () => {
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
useAuth();
const isOnTosPage = useIsOnTosPage();
const query = useQuery({
queryKey: ["settings", providerTokensSet],
queryFn: getSettingsQueryFn,
@@ -40,6 +43,7 @@ export const useSettings = () => {
retry: (_, error) => error.status !== 404,
staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes
enabled: !isOnTosPage,
meta: {
disableToast: true,
},
+11
View File
@@ -0,0 +1,11 @@
import { useLocation } from "react-router";
/**
* Hook to check if the current page is the Terms of Service acceptance page.
*
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
*/
export const useIsOnTosPage = (): boolean => {
const { pathname } = useLocation();
return pathname === "/accept-tos";
};
+4
View File
@@ -469,4 +469,8 @@ export enum I18nKey {
SYSTEM_MESSAGE_MODAL$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS",
TOS$ACCEPT_TERMS_OF_SERVICE = "TOS$ACCEPT_TERMS_OF_SERVICE",
TOS$ACCEPT_TERMS_DESCRIPTION = "TOS$ACCEPT_TERMS_DESCRIPTION",
TOS$CONTINUE = "TOS$CONTINUE",
TOS$ERROR_ACCEPTING = "TOS$ERROR_ACCEPTING",
}
+64
View File
@@ -6559,6 +6559,21 @@
"tr": "belgelendirme",
"de": "Dokumentation"
},
"AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED": {
"en": "The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.",
"ja": "アクションは実行されていません。これはユーザーが停止ボタンを押したか、リソース制約によりランタイムシステムがクラッシュして再起動したことが原因かもしれません。以前に確立されたシステム状態、依存関係、または環境変数は失われている可能性があります。",
"zh-CN": "该操作尚未执行。这可能是因为用户按下了停止按钮,或者因为运行时系统由于资源限制而崩溃并重新启动。任何先前建立的系统状态、依赖项或环境变量可能已丢失。",
"zh-TW": "該操作尚未執行。這可能是因為用戶按下了停止按鈕,或者因為運行時系統由於資源限制而崩潰並重新啟動。任何先前建立的系統狀態、依賴項或環境變數可能已丟失。",
"ko-KR": "작업이 실행되지 않았습니다. 이는 사용자가 중지 버튼을 눌렀거나 리소스 제약으로 인해 런타임 시스템이 충돌하고 재시작되었기 때문일 수 있습니다. 이전에 설정된 시스템 상태, 종속성 또는 환경 변수가 손실되었을 수 있습니다.",
"no": "Handlingen har ikke blitt utført. Dette kan ha skjedd fordi brukeren trykket på stoppknappen, eller fordi kjøretidssystemet krasjet og startet på nytt på grunn av ressursbegrensninger. Enhver tidligere etablert systemtilstand, avhengigheter eller miljøvariabler kan ha gått tapt.",
"it": "L'azione non è stata eseguita. Ciò potrebbe essere accaduto perché l'utente ha premuto il pulsante di arresto, o perché il sistema di runtime si è arrestato in modo anomalo e riavviato a causa di vincoli di risorse. Qualsiasi stato di sistema, dipendenza o variabile d'ambiente precedentemente stabilito potrebbe essere andato perso.",
"pt": "A ação não foi executada. Isso pode ter ocorrido porque o usuário pressionou o botão de parar, ou porque o sistema de tempo de execução travou e reiniciou devido a restrições de recursos. Qualquer estado do sistema, dependências ou variáveis de ambiente estabelecidos anteriormente podem ter sido perdidos.",
"es": "La acción no se ha ejecutado. Esto puede haber ocurrido porque el usuario presionó el botón de detener, o porque el sistema de tiempo de ejecución se bloqueó y reinició debido a restricciones de recursos. Cualquier estado del sistema, dependencias o variables de entorno establecidos previamente pueden haberse perdido.",
"ar": "لم يتم تنفيذ الإجراء. قد يكون هذا حدث لأن المستخدم ضغط على زر التوقف، أو لأن نظام التشغيل تعطل وأعيد تشغيله بسبب قيود الموارد. قد تكون أي حالة نظام أو تبعيات أو متغيرات بيئية تم إنشاؤها مسبقًا قد فُقدت.",
"fr": "L'action n'a pas été exécutée. Cela peut s'être produit parce que l'utilisateur a appuyé sur le bouton d'arrêt, ou parce que le système d'exécution s'est planté et a redémarré en raison de contraintes de ressources. Tout état du système, dépendances ou variables d'environnement précédemment établis peuvent avoir été perdus.",
"tr": "Eylem yürütülmedi. Bu, kullanıcının durdurma düğmesine basması veya çalışma zamanı sisteminin kaynak kısıtlamaları nedeniyle çökmesi ve yeniden başlaması nedeniyle olmuş olabilir. Daha önce kurulmuş olan herhangi bir sistem durumu, bağımlılıklar veya ortam değişkenleri kaybolmuş olabilir.",
"de": "Die Aktion wurde nicht ausgeführt. Dies kann passiert sein, weil der Benutzer die Stopp-Taste gedrückt hat oder weil das Laufzeitsystem aufgrund von Ressourcenbeschränkungen abgestürzt und neu gestartet wurde. Alle zuvor eingerichteten Systemzustände, Abhängigkeiten oder Umgebungsvariablen sind möglicherweise verloren gegangen."
},
"DIFF_VIEWER$LOADING": {
"en": "Loading...",
"ja": "読み込み中...",
@@ -6754,4 +6769,53 @@
"es": "No hay herramientas disponibles para este agente",
"tr": "Bu ajan için kullanılabilir araç yok"
}
,
"TOS$ACCEPT_TERMS_OF_SERVICE": {
"en": "Accept Terms of Service",
"ja": "利用規約に同意する",
"zh-CN": "接受服务条款",
"zh-TW": "接受服務條款",
"ko-KR": "서비스 약관 동의",
"fr": "Accepter les conditions d'utilisation",
"es": "Aceptar términos de servicio",
"de": "Nutzungsbedingungen akzeptieren",
"it": "Accetta i termini di servizio",
"pt": "Aceitar termos de serviço"
},
"TOS$ACCEPT_TERMS_DESCRIPTION": {
"en": "Please review and accept our terms of service before continuing",
"ja": "続行する前に利用規約を確認して同意してください",
"zh-CN": "请在继续之前查看并接受我们的服务条款",
"zh-TW": "請在繼續之前查看並接受我們的服務條款",
"ko-KR": "계속하기 전에 서비스 약관을 검토하고 동의해 주세요",
"fr": "Veuillez examiner et accepter nos conditions d'utilisation avant de continuer",
"es": "Por favor, revise y acepte nuestros términos de servicio antes de continuar",
"de": "Bitte überprüfen und akzeptieren Sie unsere Nutzungsbedingungen, bevor Sie fortfahren",
"it": "Si prega di rivedere e accettare i nostri termini di servizio prima di continuare",
"pt": "Por favor, revise e aceite nossos termos de serviço antes de continuar"
},
"TOS$CONTINUE": {
"en": "Continue",
"ja": "続行",
"zh-CN": "继续",
"zh-TW": "繼續",
"ko-KR": "계속",
"fr": "Continuer",
"es": "Continuar",
"de": "Fortfahren",
"it": "Continua",
"pt": "Continuar"
},
"TOS$ERROR_ACCEPTING": {
"en": "Error accepting Terms of Service",
"ja": "利用規約の承諾中にエラーが発生しました",
"zh-CN": "接受服务条款时出错",
"zh-TW": "接受服務條款時出錯",
"ko-KR": "서비스 약관 수락 중 오류 발생",
"fr": "Erreur lors de l'acceptation des conditions d'utilisation",
"es": "Error al aceptar los Términos de Servicio",
"de": "Fehler beim Akzeptieren der Nutzungsbedingungen",
"it": "Errore nell'accettazione dei Termini di Servizio",
"pt": "Erro ao aceitar os Termos de Serviço"
}
}
+1
View File
@@ -8,6 +8,7 @@ import {
export default [
layout("routes/root-layout.tsx", [
index("routes/home.tsx"),
route("accept-tos", "routes/accept-tos.tsx"),
route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"),
route("git", "routes/git-settings.tsx"),
+84
View File
@@ -0,0 +1,84 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router";
import { useMutation } from "@tanstack/react-query";
import { I18nKey } from "#/i18n/declaration";
import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { TOSCheckbox } from "#/components/features/waitlist/tos-checkbox";
import { BrandButton } from "#/components/features/settings/brand-button";
import { handleCaptureConsent } from "#/utils/handle-capture-consent";
import { openHands } from "#/api/open-hands-axios";
export default function AcceptTOS() {
const { t } = useTranslation();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
// Get the redirect URL from the query parameters
const redirectUrl = searchParams.get("redirect_url") || "/";
// Use mutation for accepting TOS
const { mutate: acceptTOS, isPending: isSubmitting } = useMutation({
mutationFn: async () => {
// Set consent for analytics
handleCaptureConsent(true);
// Call the API to record TOS acceptance in the database
return openHands.post("/api/accept_tos", {
redirect_url: redirectUrl,
});
},
onSuccess: (response) => {
// Get the redirect URL from the response
const finalRedirectUrl = response.data.redirect_url || redirectUrl;
// Check if the redirect URL is an external URL (starts with http or https)
if (
finalRedirectUrl.startsWith("http://") ||
finalRedirectUrl.startsWith("https://")
) {
// For external URLs, redirect using window.location
window.location.href = finalRedirectUrl;
} else {
// For internal routes, use navigate
navigate(finalRedirectUrl);
}
},
});
const handleAcceptTOS = () => {
if (isTosAccepted && !isSubmitting) {
acceptTOS();
}
};
return (
<div className="flex flex-col items-center justify-center h-full">
<div className="border border-tertiary p-8 rounded-lg max-w-md w-full flex flex-col gap-6 items-center bg-base-secondary">
<AllHandsLogo width={68} height={46} />
<div className="flex flex-col gap-2 w-full items-center text-center">
<h1 className="text-2xl font-bold">
{t(I18nKey.TOS$ACCEPT_TERMS_OF_SERVICE)}
</h1>
<p className="text-sm text-gray-500">
{t(I18nKey.TOS$ACCEPT_TERMS_DESCRIPTION)}
</p>
</div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<BrandButton
isDisabled={!isTosAccepted || isSubmitting}
type="button"
variant="primary"
onClick={handleAcceptTOS}
className="w-full"
>
{isSubmitting ? t(I18nKey.HOME$LOADING) : t(I18nKey.TOS$CONTINUE)}
</BrandButton>
</div>
</div>
);
}
+56 -28
View File
@@ -21,6 +21,7 @@ import { useMigrateUserConsent } from "#/hooks/use-migrate-user-consent";
import { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export function ErrorBoundary() {
const error = useRouteError();
@@ -58,6 +59,7 @@ export function ErrorBoundary() {
export default function MainApp() {
const navigate = useNavigate();
const { pathname } = useLocation();
const tosPageStatus = useIsOnTosPage();
const [searchParams] = useSearchParams();
const { data: settings } = useSettings();
const { error, isFetching } = useBalance();
@@ -71,49 +73,75 @@ export default function MainApp() {
isError: authError,
} = useIsAuthed();
// Always call the hook, but we'll only use the result when not on TOS page
const gitHubAuthUrl = useGitHubAuthUrl({
appMode: config.data?.APP_MODE || null,
gitHubClientId: config.data?.GITHUB_CLIENT_ID || null,
});
// When on TOS page, we don't use the GitHub auth URL
const effectiveGitHubAuthUrl = tosPageStatus ? null : gitHubAuthUrl;
const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => {
if (settings?.LANGUAGE) {
// Don't change language when on TOS page
if (!tosPageStatus && settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE);
}
}, [settings?.LANGUAGE]);
}, [settings?.LANGUAGE, tosPageStatus]);
React.useEffect(() => {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
// Don't show consent form when on TOS page
if (!tosPageStatus) {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
setConsentFormIsOpen(consentFormModalIsOpen);
}, [settings]);
React.useEffect(() => {
// Migrate user consent to the server if it was previously stored in localStorage
migrateUserConsent({
handleAnalyticsWasPresentInLocalStorage: () => {
setConsentFormIsOpen(false);
},
});
}, []);
React.useEffect(() => {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (!isFetching && searchParams.get("free_credits") === "success") {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
searchParams.delete("free_credits");
navigate("/");
setConsentFormIsOpen(consentFormModalIsOpen);
}
}, [error?.status, pathname, isFetching]);
}, [settings, tosPageStatus]);
const userIsAuthed = !!isAuthed && !authError;
React.useEffect(() => {
// Don't migrate user consent when on TOS page
if (!tosPageStatus) {
// Migrate user consent to the server if it was previously stored in localStorage
migrateUserConsent({
handleAnalyticsWasPresentInLocalStorage: () => {
setConsentFormIsOpen(false);
},
});
}
}, [tosPageStatus]);
React.useEffect(() => {
// Don't do any redirects when on TOS page
if (!tosPageStatus) {
// Don't allow users to use the app if it 402s
if (error?.status === 402 && pathname !== "/") {
navigate("/");
} else if (
!isFetching &&
searchParams.get("free_credits") === "success"
) {
displaySuccessToast(t(I18nKey.BILLING$YOURE_IN));
searchParams.delete("free_credits");
navigate("/");
}
}
}, [error?.status, pathname, isFetching, tosPageStatus]);
// When on TOS page, we don't make any API calls, so we need to handle this case
const userIsAuthed = tosPageStatus ? false : !!isAuthed && !authError;
// Only show the auth modal if:
// 1. User is not authenticated
// 2. We're not currently on the TOS page
// 3. We're in SaaS mode
const renderAuthModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas";
!isFetchingAuth &&
!userIsAuthed &&
!tosPageStatus &&
config.data?.APP_MODE === "saas";
return (
<div
@@ -131,7 +159,7 @@ export default function MainApp() {
{renderAuthModal && (
<AuthModal
githubAuthUrl={gitHubAuthUrl}
githubAuthUrl={effectiveGitHubAuthUrl}
appMode={config.data?.APP_MODE}
/>
)}
+20
View File
@@ -0,0 +1,20 @@
/**
* Checks if the current page is the Terms of Service acceptance page.
* This function works outside of React Router context by checking window.location directly.
*
* @param {string} [pathname] - Optional pathname from React Router's useLocation hook
* @returns {boolean} True if the current page is the TOS acceptance page, false otherwise.
*/
export const isOnTosPage = (pathname?: string): boolean => {
// If pathname is provided (from React Router), use it
if (pathname !== undefined) {
return pathname === "/accept-tos";
}
// Otherwise check window.location (works outside React Router context)
if (typeof window !== "undefined") {
return window.location.pathname === "/accept-tos";
}
return false;
};
+4
View File
@@ -18,6 +18,10 @@ vi.mock("react-i18next", async (importOriginal) => ({
}),
}));
vi.mock("#/hooks/use-is-on-tos-page", () => ({
useIsOnTosPage: () => false,
}));
// Mock requests during tests
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterEach(() => {
@@ -44,8 +44,9 @@ Your primary role is to assist users by executing commands, modifying code, and
* For bug fixes: Create tests to verify issues before implementing fixes
* For new features: Consider test-driven development when appropriate
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
* If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
5. VERIFICATION: Test your implementation thoroughly, including edge cases
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
</PROBLEM_SOLVING_WORKFLOW>
<SECURITY>
+8 -1
View File
@@ -76,6 +76,8 @@ from openhands.llm.metrics import Metrics, TokenUsage
TRAFFIC_CONTROL_REMINDER = (
"Please click on resume button if you'd like to continue, or start a new task."
)
ERROR_ACTION_NOT_EXECUTED_ID = 'AGENT_ERROR$ERROR_ACTION_NOT_EXECUTED'
ERROR_ACTION_NOT_EXECUTED = 'The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.'
class AgentController:
@@ -566,7 +568,10 @@ class AgentController:
# make a new ErrorObservation with the tool call metadata
if not found_observation:
obs = ErrorObservation(content='The action has not been executed.')
obs = ErrorObservation(
content=ERROR_ACTION_NOT_EXECUTED,
error_id=ERROR_ACTION_NOT_EXECUTED_ID,
)
obs.tool_call_metadata = self._pending_action.tool_call_metadata
obs._cause = self._pending_action.id # type: ignore[attr-defined]
self.event_stream.add_event(obs, EventSource.AGENT)
@@ -842,6 +847,8 @@ class AgentController:
'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str
or 'input length and `max_tokens` exceed context limit' in error_str
or 'please reduce the length of either one'
in error_str # For OpenRouter context window errors
or isinstance(e, ContextWindowExceededError)
):
if self.agent.config.enable_history_truncation:
+6 -2
View File
@@ -60,6 +60,10 @@ PROVIDER_TOKEN_TYPE_WITH_JSON_SCHEMA = Annotated[
PROVIDER_TOKEN_TYPE,
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
]
CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA = Annotated[
CUSTOM_SECRETS_TYPE,
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}),
]
class SecretStore(BaseModel):
@@ -67,8 +71,8 @@ class SecretStore(BaseModel):
default_factory=lambda: MappingProxyType({})
)
custom_secrets: CUSTOM_SECRETS_TYPE = Field(
default_factory=lambda: MappingProxyType({})
custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA = Field(
default_factory=lambda: MappingProxyType({}),
)
model_config = {
+2 -2
View File
@@ -230,8 +230,8 @@ class ConversationMemory:
pending_tool_call_action_messages[llm_response.id] = Message(
role=getattr(assistant_msg, 'role', 'assistant'),
# tool call content SHOULD BE a string
content=[TextContent(text=assistant_msg.content or '')]
if assistant_msg.content is not None
content=[TextContent(text=assistant_msg.content)]
if assistant_msg.content and assistant_msg.content.strip()
else [],
tool_calls=assistant_msg.tool_calls,
)
+116 -66
View File
@@ -1,141 +1,191 @@
# OpenHands GitHub & GitLab Issue Resolver 🙌
# OpenHands Github & Gitlab Issue Resolver 🙌
Need help resolving GitHub or GitLab issues? Let an AI agent help you out!
Need help resolving a GitHub issue but don't have the time to do it yourself? Let an AI agent help you out!
This tool uses [OpenHands](https://github.com/all-hands-ai/openhands) AI agents to automatically resolve issues in your repositories. It's designed to handle one issue at a time with high quality.
This tool allows you to use open-source AI agents based on [OpenHands](https://github.com/all-hands-ai/openhands)
to attempt to resolve GitHub issues automatically. While it can handle multiple issues, it's primarily designed
to help you resolve one issue at a time with high quality.
## 1. Setting Up for GitHub (Action Workflow)
Getting started is simple - just follow the instructions below.
### Prerequisites
## Using the GitHub Actions Workflow
- [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for
This repository includes a GitHub Actions workflow that can automatically attempt to fix individual issues labeled with 'fix-me'.
Follow these steps to use this workflow in your own repository:
- "contents"
- "issues"
- "pull requests"
- "workflows"
1. [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for "contents", "issues", "pull requests", and "workflows"
- Create an LLM API key (e,g [Claude API](https://www.anthropic.com/api))
Note: If you're working with an organizational repository, you may need to configure the organization's personal access token policy first. See [Setting a personal access token policy for your organization](https://docs.github.com/en/organizations/managing-programmatic-access-to-your-organization/setting-a-personal-access-token-policy-for-your-organization) for details.
### Installation
2. Create an API key for the [Claude API](https://www.anthropic.com/api) (recommended) or another supported LLM service
1. Copy `examples/openhands-resolver.yml` to your repository's `.github/workflows/` directory
3. Copy `examples/openhands-resolver.yml` to your repository's `.github/workflows/` directory
2. Configure repository permissions:
4. Configure repository permissions:
- Go to `Settings -> Actions -> General -> Workflow permissions`
- Select "Read and write permissions"
- Enable "Allow Github Actions to create and approve pull requests"
- Go to `Settings -> Actions -> General -> Workflow permissions`
- Select **Read and write permissions**
- Enable **Allow Github Actions to create and approve pull requests**
> If "Read and write permissions" is greyed out:
>
> - Check organization settings first
> - Otherwise, permissions might need to be set in [Enterprise policy settings](https://docs.github.com/en/enterprise-cloud@latest/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise#enforcing-a-policy-for-workflow-permissions-in-your-enterprise)
3. Set up [GitHub secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions):
Note: If the "Read and write permissions" option is greyed out:
- First check if permissions need to be set at the organization level
- If still greyed out at the organization level, permissions need to be set in the [Enterprise policy settings](https://docs.github.com/en/enterprise-cloud@latest/admin/enforcing-policies/enforcing-policies-for-your-enterprise/enforcing-policies-for-github-actions-in-your-enterprise#enforcing-a-policy-for-workflow-permissions-in-your-enterprise)
5. Set up [GitHub secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions):
- Required:
- `LLM_API_KEY`: Your LLM API key
- `LLM_API_KEY`: Your LLM API key
- Optional:
- `PAT_USERNAME`: GitHub username for the personal access token
- `PAT_TOKEN`: The personal access token
- `LLM_BASE_URL`: Base URL for LLM API (only if using a proxy)
- [See how to customize more configurations](https://docs.all-hands.dev/modules/usage/how-to/github-action#custom-configurations)
## 2. Setting up GitLab (CI Runner)
Note: You can set these secrets at the organization level to use across multiple repositories.
### Prerequisites
6. Set up any [custom configurations required](https://docs.all-hands.dev/modules/usage/how-to/github-action#custom-configurations)
Create a GitLab Personal Access Token with API, read/write access
7. Usage:
There are two ways to trigger the OpenHands agent:
### Installation
a. Using the 'fix-me' label:
- Add the 'fix-me' label to any issue you want the AI to resolve
- The agent will consider all comments in the issue thread when resolving
- The workflow will:
1. Attempt to resolve the issue using OpenHands
2. Create a draft PR if successful, or push a branch if unsuccessful
3. Comment on the issue with the results
4. Remove the 'fix-me' label once processed
## 3. Triggering OpenHands Agent
b. Using `@openhands-agent` mention:
- Create a new comment containing `@openhands-agent` in any issue
- The agent will only consider the comment where it's mentioned
- The workflow will:
1. Attempt to resolve the issue based on the specific comment
2. Create a draft PR if successful, or push a branch if unsuccessful
3. Comment on the issue with the results
You can trigger OpenHands in two shared ways (works for both GitHub and GitLab):
Need help? Feel free to [open an issue](https://github.com/all-hands-ai/openhands/issues) or email us at [contact@all-hands.dev](mailto:contact@all-hands.dev).
Using the 'fix-me' label:
## Manual Installation
- Add the 'fix-me' label to any issue you want the AI to resolve
- The agent will consider all comments in the issue thread when resolving
If you prefer to run the resolver programmatically instead of using GitHub Actions, follow these steps:
Using `@openhands-agent` in an issue/pr comment:
- Create a new comment containing `@openhands-agent`
- The agent will only consider the comment + comment thread where it's mentioned
## 4. Running Locally
### Installation
1. Install the package:
```bash
pip install openhands-ai
```
### Setup
2. Create a GitHub or GitLab access token:
- Create a GitHub acces token
- Visit [GitHub's token settings](https://github.com/settings/personal-access-tokens/new)
- Create a fine-grained token with these scopes:
- "Content"
- "Pull requests"
- "Issues"
- "Workflows"
- If you don't have push access to the target repo, you can fork it first
Create a GitHub or GitLab access token with appropriate permissions
- Create a GitLab acces token
- Visit [GitLab's token settings](https://gitlab.com/-/user_settings/personal_access_tokens)
- Create a fine-grained token with these scopes:
- 'api'
- 'read_api'
- 'read_user'
- 'read_repository'
- 'write_repository'
Set up environment variables:
3. Set up environment variables:
```bash
# GitHub credentials
export GITHUB_TOKEN="your-github-token"
export GIT_USERNAME="your-github-username"
# GitLab credentials (if using GitLab)
# GitHub credentials
export GITHUB_TOKEN="your-github-token"
export GIT_USERNAME="your-github-username" # Optional, defaults to token owner
# GitLab credentials if you're using GitLab repo
export GITLAB_TOKEN="your-gitlab-token"
export GIT_USERNAME="your-gitlab-username"
export GIT_USERNAME="your-gitlab-username" # Optional, defaults to token owner
# LLM configuration
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022"
export LLM_MODEL="anthropic/claude-3-5-sonnet-20241022" # Recommended
export LLM_API_KEY="your-llm-api-key"
export LLM_BASE_URL="your-api-url" # Optional
export LLM_BASE_URL="your-api-url" # Optional, for API proxies
```
### Resolving Issues
Note: OpenHands works best with powerful models like Anthropic's Claude or OpenAI's GPT-4. While other models are supported, they may not perform as well for complex issue resolution.
Resolve a single issue:
## Resolving Issues
The resolver can automatically attempt to fix a single issue in your repository using the following command:
```bash
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER]
```
### Responding to PR Comments
For instance, if you want to resolve issue #100 in this repo, you would run:
Respond to comments on pull requests:
```bash
python -m openhands.resolver.resolve_issue --selected-repo all-hands-ai/openhands --issue-number 100
```
The output will be written to the `output/` directory.
If you've installed the package from source using poetry, you can use:
```bash
poetry run python openhands/resolver/resolve_issue.py --selected-repo all-hands-ai/openhands --issue-number 100
```
## Responding to PR Comments
The resolver can also respond to comments on pull requests using:
```bash
python -m openhands.resolver.send_pull_request --issue-number PR_NUMBER --issue-type pr
```
### Visualizing Results
This functionality is available both through the GitHub Actions workflow and when running the resolver locally.
View successful PRs:
## Visualizing successful PRs
To find successful PRs, you can run the following command:
```bash
grep '"success":true' output/output.jsonl | sed 's/.*\("number":[0-9]*\).*/\1/g'
```
Visualize specific PR:
Then you can go through and visualize the ones you'd like.
```bash
python -m openhands.resolver.visualize_resolver_output --issue-number ISSUE_NUMBER --vis-method json
```
### Uploading PRs
## Uploading PRs
Upload your changes in one of three ways:
If you find any PRs that were successful, you can upload them.
There are three ways you can upload:
1. `branch` - upload a branch without creating a PR
2. `draft` - create a draft PR
3. `ready` - create a non-draft PR that's ready for review
```bash
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type [branch|draft|ready]
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type draft
```
## Custom Instructions
If you want to upload to a fork, you can do so by specifying the `fork-owner`:
Add repository-specific instructions by creating a file at `.openhands/microagents/repo.md` in your repository. For more information about repository microagents, see [Repository Instructions](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents#2-repository-instructions-private).
```bash
python -m openhands.resolver.send_pull_request --issue-number ISSUE_NUMBER --username YOUR_GITHUB_OR_GITLAB_USERNAME --pr-type draft --fork-owner YOUR_GITHUB_OR_GITLAB_USERNAME
```
## Providing Custom Instructions
You can customize how the AI agent approaches issue resolution by adding a repository microagent file at `.openhands/microagents/repo.md` in your repository. This file's contents will be automatically loaded in the prompt when working with your repository. For more information about repository microagents, see [Repository Instructions](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents#2-repository-instructions-private).
## Troubleshooting
If you have any issues, please open an issue on this github repo, we're happy to help!
If you have any issues, please open an issue on this github or gitlab repo, we're happy to help!
Alternatively, you can [email us](mailto:contact@all-hands.dev) or join the OpenHands Slack workspace (see [the README](/README.md) for an invite link).
+80 -22
View File
@@ -98,14 +98,21 @@ class RemoteRuntime(ActionExecutionClient):
return self.runtime_url
async def connect(self):
self.log('debug', f'Connecting to remote runtime with session ID: {self.sid}')
try:
self.log('debug', 'Starting or attaching to runtime...')
await call_sync_from_async(self._start_or_attach_to_runtime)
except Exception:
self.log('debug', 'Runtime started/attached successfully')
except Exception as e:
self.log('error', f'Runtime failed to start: {str(e)}')
self.close()
self.log('error', 'Runtime failed to start')
raise
self.log('debug', 'Setting up initial environment...')
await call_sync_from_async(self.setup_initial_env)
self.log('debug', 'Initial environment setup complete')
self._runtime_initialized = True
self.log('info', f'Remote runtime connection established. Runtime ID: {self.runtime_id}, URL: {self.runtime_url}')
def _start_or_attach_to_runtime(self):
existing_runtime = self._check_existing_runtime()
@@ -145,17 +152,21 @@ class RemoteRuntime(ActionExecutionClient):
self.send_status_message(' ')
def _check_existing_runtime(self) -> bool:
self.log('debug', f'Checking for existing runtime with session ID: {self.sid}')
try:
response = self._send_runtime_api_request(
'GET',
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
)
data = response.json()
self.log('debug', f'Existing runtime check response: {data}')
status = data.get('status')
if status == 'running' or status == 'paused':
self.log('debug', f'Found existing runtime with status: {status}')
self._parse_runtime_response(response)
except httpx.HTTPError as e:
if e.response.status_code == 404:
self.log('debug', f'No existing runtime found for session ID: {self.sid}')
return False
self.log('debug', f'Error while looking for remote runtime: {e}')
raise
@@ -167,6 +178,7 @@ class RemoteRuntime(ActionExecutionClient):
raise
if status == 'running':
self.log('info', f'Using existing running runtime for session ID: {self.sid}')
return True
elif status == 'stopped':
self.log('debug', 'Found existing remote runtime, but it is stopped')
@@ -224,16 +236,20 @@ class RemoteRuntime(ActionExecutionClient):
)
def _start_runtime(self):
self.log('debug', 'Starting remote runtime...')
# Prepare the request body for the /start endpoint
command = get_action_execution_server_startup_command(
server_port=self.port,
plugins=self.plugins,
app_config=self.config,
)
self.log('debug', f'Action execution server command: {command}')
environment = {}
if self.config.debug or os.environ.get('DEBUG', 'false').lower() == 'true':
environment['DEBUG'] = 'true'
environment.update(self.config.sandbox.runtime_startup_env_vars)
start_request = {
'image': self.container_image,
'command': command,
@@ -242,10 +258,16 @@ class RemoteRuntime(ActionExecutionClient):
'session_id': self.sid,
'resource_factor': self.config.sandbox.remote_runtime_resource_factor,
}
if self.config.sandbox.remote_runtime_class == 'sysbox':
start_request['runtime_class'] = 'sysbox-runc'
self.log('debug', 'Using sysbox runtime class')
else:
self.log('debug', 'Using default (gvisor) runtime class')
# We ignore other runtime classes for now, because both None and 'gvisor' map to 'gvisor'
self.log('debug', f'Sending start request with parameters: {start_request}')
# Start the sandbox using the /start endpoint
try:
response = self._send_runtime_api_request(
@@ -255,8 +277,8 @@ class RemoteRuntime(ActionExecutionClient):
)
self._parse_runtime_response(response)
self.log(
'debug',
f'Runtime started. URL: {self.runtime_url}',
'info',
f'Runtime started successfully. Runtime ID: {self.runtime_id}, URL: {self.runtime_url}',
)
except httpx.HTTPError as e:
self.log('error', f'Unable to start runtime: {str(e)}')
@@ -269,15 +291,27 @@ class RemoteRuntime(ActionExecutionClient):
3. Poll for the runtime to be ready
4. Update env vars
"""
self.log('debug', f'Resuming paused runtime with ID: {self.runtime_id}')
self.send_status_message('STATUS$STARTING_RUNTIME')
self._send_runtime_api_request(
'POST',
f'{self.config.sandbox.remote_runtime_api_url}/resume',
json={'runtime_id': self.runtime_id},
)
self._wait_until_alive()
self.setup_initial_env()
self.log('debug', 'Runtime resumed.')
try:
self.log('debug', 'Sending resume request to runtime API')
self._send_runtime_api_request(
'POST',
f'{self.config.sandbox.remote_runtime_api_url}/resume',
json={'runtime_id': self.runtime_id},
)
self.log('debug', 'Waiting for resumed runtime to become alive')
self._wait_until_alive()
self.log('debug', 'Setting up initial environment for resumed runtime')
self.setup_initial_env()
self.log('info', f'Runtime with ID {self.runtime_id} resumed successfully')
except Exception as e:
self.log('error', f'Failed to resume runtime: {str(e)}')
raise
def _parse_runtime_response(self, response: httpx.Response):
start_response = response.json()
@@ -324,53 +358,62 @@ class RemoteRuntime(ActionExecutionClient):
return retry_decorator(self._wait_until_alive_impl)()
def _wait_until_alive_impl(self):
self.log('debug', f'Waiting for runtime to be alive at url: {self.runtime_url}')
self.log('debug', f'Checking if runtime is alive at url: {self.runtime_url}')
runtime_info_response = self._send_runtime_api_request(
'GET',
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
)
runtime_data = runtime_info_response.json()
assert 'runtime_id' in runtime_data
assert runtime_data['runtime_id'] == self.runtime_id
assert 'pod_status' in runtime_data
self.log('debug', f'Runtime info response: {runtime_data}')
assert 'runtime_id' in runtime_data, "Missing runtime_id in response"
assert runtime_data['runtime_id'] == self.runtime_id, f"Runtime ID mismatch: {runtime_data['runtime_id']} != {self.runtime_id}"
assert 'pod_status' in runtime_data, "Missing pod_status in response"
pod_status = runtime_data['pod_status'].lower()
self.log('debug', f'Pod status: {pod_status}')
restart_count = runtime_data.get('restart_count', 0)
if restart_count != 0:
restart_reasons = runtime_data.get('restart_reasons')
self.log(
'debug', f'Pod restarts: {restart_count}, reasons: {restart_reasons}'
'warning', f'Pod has restarted {restart_count} times, reasons: {restart_reasons}'
)
# FIXME: We should fix it at the backend of /start endpoint, make sure
# the pod is created before returning the response.
# Retry a period of time to give the cluster time to start the pod
if pod_status == 'ready':
self.log('debug', 'Pod status is ready, checking if action execution server is alive')
try:
self.check_if_alive()
self.log('info', 'Runtime is fully alive and ready to accept commands')
return
except httpx.HTTPError as e:
self.log(
'warning',
f"Runtime /alive failed, but pod says it's ready: {str(e)}",
f"Runtime /alive check failed, but pod says it's ready: {str(e)}",
)
raise AgentRuntimeNotReadyError(
f'Runtime /alive failed to respond with 200: {str(e)}'
)
return
elif (
pod_status == 'not found'
or pod_status == 'pending'
or pod_status == 'running'
): # nb: Running is not yet Ready
self.log('debug', f'Pod is in transitional state: {pod_status}')
raise AgentRuntimeNotReadyError(
f'Runtime (ID={self.runtime_id}) is not yet ready. Status: {pod_status}'
)
elif pod_status in ('failed', 'unknown', 'crashloopbackoff'):
if pod_status == 'crashloopbackoff':
self.log('error', f'Pod is in CrashLoopBackOff state. Restart count: {restart_count}')
raise AgentRuntimeUnavailableError(
'Runtime crashed and is being restarted, potentially due to memory usage. Please try again.'
)
else:
self.log('error', f'Pod is in failed state: {pod_status}')
raise AgentRuntimeUnavailableError(
f'Runtime is unavailable (status: {pod_status}). Please try again.'
)
@@ -385,11 +428,18 @@ class RemoteRuntime(ActionExecutionClient):
raise AgentRuntimeNotReadyError()
def close(self):
self.log('debug', f'Closing remote runtime with ID: {self.runtime_id}')
if self.attach_to_existing:
self.log('debug', 'Runtime was attached to existing instance, not stopping it')
super().close()
return
if self.config.sandbox.keep_runtime_alive:
self.log('debug', 'keep_runtime_alive is set, not stopping the runtime')
if self.config.sandbox.pause_closed_runtimes:
self.log('debug', 'pause_closed_runtimes is set, pausing the runtime')
try:
if not self._runtime_closed:
self._send_runtime_api_request(
@@ -397,12 +447,14 @@ class RemoteRuntime(ActionExecutionClient):
f'{self.config.sandbox.remote_runtime_api_url}/pause',
json={'runtime_id': self.runtime_id},
)
self.log('debug', 'Runtime paused.')
self.log('info', f'Runtime with ID {self.runtime_id} paused successfully')
except Exception as e:
self.log('error', f'Unable to pause runtime: {str(e)}')
raise e
super().close()
return
self.log('debug', 'Stopping the runtime')
try:
if not self._runtime_closed:
self._send_runtime_api_request(
@@ -410,7 +462,7 @@ class RemoteRuntime(ActionExecutionClient):
f'{self.config.sandbox.remote_runtime_api_url}/stop',
json={'runtime_id': self.runtime_id},
)
self.log('debug', 'Runtime stopped.')
self.log('info', f'Runtime with ID {self.runtime_id} stopped successfully')
except Exception as e:
self.log('error', f'Unable to stop runtime: {str(e)}')
raise e
@@ -418,15 +470,21 @@ class RemoteRuntime(ActionExecutionClient):
super().close()
def _send_runtime_api_request(self, method, url, **kwargs):
self.log('debug', f'Sending {method} request to {url} with kwargs: {kwargs}')
try:
kwargs['timeout'] = self.config.sandbox.remote_runtime_api_timeout
return send_request(self.session, method, url, **kwargs)
response = send_request(self.session, method, url, **kwargs)
self.log('debug', f'Received response from {url}: status={response.status_code}')
return response
except httpx.TimeoutException:
self.log(
'error',
f'No response received within the timeout period for url: {url}',
)
raise
except httpx.HTTPError as e:
self.log('error', f'HTTP error when calling {url}: {str(e)}')
raise
def _send_action_server_request(self, method, url, **kwargs):
if not self.config.sandbox.remote_runtime_enable_retries:
+8 -1
View File
@@ -45,7 +45,14 @@ def create_provider_tokens_object(
async def connect(connection_id: str, environ):
logger.info(f'sio:connect: {connection_id}')
query_params = parse_qs(environ.get('QUERY_STRING', ''))
latest_event_id = int(query_params.get('latest_event_id', [-1])[0])
latest_event_id_str = query_params.get('latest_event_id', [-1])[0]
try:
latest_event_id = int(latest_event_id_str)
except ValueError:
logger.debug(
f'Invalid latest_event_id value: {latest_event_id_str}, defaulting to -1'
)
latest_event_id = -1
conversation_id = query_params.get('conversation_id', [None])[0]
raw_list = query_params.get('providers_set', [])
providers_list = []
Generated
+29 -29
View File
@@ -207,14 +207,14 @@ vertex = ["google-auth[requests] (>=2,<3)"]
[[package]]
name = "anyio"
version = "4.8.0"
version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation", "runtime", "test"]
files = [
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"},
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"},
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
]
[package.dependencies]
@@ -223,8 +223,8 @@ sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
@@ -496,18 +496,18 @@ files = [
[[package]]
name = "boto3"
version = "1.38.3"
version = "1.38.4"
description = "The AWS SDK for Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "boto3-1.38.3-py3-none-any.whl", hash = "sha256:9218f86e2164e1bddb75d435bbde4fa651aa58687213d7e3e1b50f7eb8868f66"},
{file = "boto3-1.38.3.tar.gz", hash = "sha256:655d51abcd68a40a33c52dbaa2ca73fc63c746b894e2ae22ed8ddc1912ddd93f"},
{file = "boto3-1.38.4-py3-none-any.whl", hash = "sha256:ab315d38409f5b3262b653a10b0fac786bcff7e51e03dcb99ff38ba16bf85630"},
{file = "boto3-1.38.4.tar.gz", hash = "sha256:4990df0087fe7be944ba06c2d1e6512b5a24f821af5a4881f24309e13ae29e68"},
]
[package.dependencies]
botocore = ">=1.38.3,<1.39.0"
botocore = ">=1.38.4,<1.39.0"
jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.12.0,<0.13.0"
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]]
name = "boto3-stubs"
version = "1.38.3"
description = "Type annotations for boto3 1.38.3 generated with mypy-boto3-builder 8.10.1"
version = "1.38.4"
description = "Type annotations for boto3 1.38.4 generated with mypy-boto3-builder 8.10.1"
optional = false
python-versions = ">=3.8"
groups = ["evaluation"]
files = [
{file = "boto3_stubs-1.38.3-py3-none-any.whl", hash = "sha256:93a2c38987dd0ee19a661e8fd9a77fb4b4a30e56f63115701c307bfc55e2695c"},
{file = "boto3_stubs-1.38.3.tar.gz", hash = "sha256:e406626de8daf537984678355ad0e32d838865c4ea3d223268964d4e6fb44534"},
{file = "boto3_stubs-1.38.4-py3-none-any.whl", hash = "sha256:ae931b5f40ebf70cd7ddc36065d058d406c6aaecc766bd843be855487fc6345e"},
{file = "boto3_stubs-1.38.4.tar.gz", hash = "sha256:eff7f741a9b9df5b0af1e7018878a56028f05bb3d9c57cd50cc4f60bdae7960b"},
]
[package.dependencies]
@@ -579,7 +579,7 @@ bedrock-data-automation-runtime = ["mypy-boto3-bedrock-data-automation-runtime (
bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"]
billing = ["mypy-boto3-billing (>=1.38.0,<1.39.0)"]
billingconductor = ["mypy-boto3-billingconductor (>=1.38.0,<1.39.0)"]
boto3 = ["boto3 (==1.38.3)"]
boto3 = ["boto3 (==1.38.4)"]
braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"]
budgets = ["mypy-boto3-budgets (>=1.38.0,<1.39.0)"]
ce = ["mypy-boto3-ce (>=1.38.0,<1.39.0)"]
@@ -943,14 +943,14 @@ xray = ["mypy-boto3-xray (>=1.38.0,<1.39.0)"]
[[package]]
name = "botocore"
version = "1.38.3"
version = "1.38.4"
description = "Low-level, data-driven core of boto 3."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "botocore-1.38.3-py3-none-any.whl", hash = "sha256:96f823240fe3704b99c17d1d1b2fd2d1679cf56d2a55b095f00255b76087cbf0"},
{file = "botocore-1.38.3.tar.gz", hash = "sha256:790f8f966201781f5fcf486d48b4492e9f734446bbf9d19ef8159d08be854243"},
{file = "botocore-1.38.4-py3-none-any.whl", hash = "sha256:6206cf07be1069efaead2ddc858eb752dafef276ebbe88ac32b5c427b1d90570"},
{file = "botocore-1.38.4.tar.gz", hash = "sha256:6143546bb56f1da4dff8d285cb6a3b8b0b6442451fe5937cb48a62bf7275386f"},
]
[package.dependencies]
@@ -2663,7 +2663,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi
grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0"
requests = ">=2.18.0,<3.0.0.dev0"
@@ -2878,7 +2878,7 @@ google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
{version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@@ -3794,14 +3794,14 @@ files = [
[[package]]
name = "json-repair"
version = "0.43.0"
version = "0.44.0"
description = "A package to repair broken json strings"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "json_repair-0.43.0-py3-none-any.whl", hash = "sha256:3f2b66819c9f5e29edd5dd4851223b72d10ed816b6423b3c92e424090c3ffc1d"},
{file = "json_repair-0.43.0.tar.gz", hash = "sha256:77cc6eda6f407ff5fe9544f962e42b332cca1e8c9f3f9f9dc660327028e0d651"},
{file = "json_repair-0.44.0-py3-none-any.whl", hash = "sha256:4646b59decf8763c20cd218079a12e334202d658d760ddd999f23812f07bb9cd"},
{file = "json_repair-0.44.0.tar.gz", hash = "sha256:732469762471e535de8aadcb4ec5a4b47e96c677d036cbc1c78ab396925c6a71"},
]
[[package]]
@@ -4882,14 +4882,14 @@ files = [
[[package]]
name = "modal"
version = "0.74.30"
version = "0.74.35"
description = "Python client library for Modal"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
files = [
{file = "modal-0.74.30-py3-none-any.whl", hash = "sha256:46006cb57309171fe36ee41528a7cc8c0e67c88afd9bf04a9900313c18925aa4"},
{file = "modal-0.74.30.tar.gz", hash = "sha256:14bd2ea0ebc9ab1ebce29ea76ddf12047f23599983725c5f82990ae97bea05c7"},
{file = "modal-0.74.35-py3-none-any.whl", hash = "sha256:845c3176e5fc2d0856ff1d88a5fc7699552e6542135aedae6cd52598faa55fc0"},
{file = "modal-0.74.35.tar.gz", hash = "sha256:bef2a40f18a40514e7502dbe543fc026b0a2542597669cd0bc0fa0db1149600a"},
]
[package.dependencies]
@@ -5633,14 +5633,14 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
[[package]]
name = "openhands-aci"
version = "0.2.10"
version = "0.2.11"
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
optional = false
python-versions = "<4.0,>=3.12"
groups = ["main"]
files = [
{file = "openhands_aci-0.2.10-py3-none-any.whl", hash = "sha256:0703eb117e24326d80d990b81a0c71e28a364f56999095b95c146c934e40fc55"},
{file = "openhands_aci-0.2.10.tar.gz", hash = "sha256:a5e6bf46cbd9a99c5c592548419a9a3b3091c4e82f0227e8aaf470b18a261cc9"},
{file = "openhands_aci-0.2.11-py3-none-any.whl", hash = "sha256:bb6cdd30da8cf57292f7d1d87b2396a31af3f3abc2f5d6f02c22cb648956b41e"},
{file = "openhands_aci-0.2.11.tar.gz", hash = "sha256:38ae6abbeedfc0f6018dc5db58ad0ccc38295d649fe27218a0acab689df0e6c7"},
]
[package.dependencies]
@@ -10269,4 +10269,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "d3f933e9abf6be481ec137e14f8f7ac502afd591a9ba74b315737fd894ca5cfe"
content-hash = "e8326a1441d5ce74c017755566e1e0d865551712290c00202d257c931b7dc5bd"
+3 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "openhands-ai"
version = "0.34.0"
version = "0.35.0"
description = "OpenHands: Code Less, Make More"
authors = ["OpenHands"]
license = "MIT"
@@ -62,7 +62,7 @@ runloop-api-client = "0.32.0"
libtmux = ">=0.37,<0.40"
pygithub = "^2.5.0"
joblib = "*"
openhands-aci = "^0.2.10"
openhands-aci = "0.2.11"
python-socketio = "^5.11.4"
redis = "^5.2.0"
sse-starlette = "^2.1.3"
@@ -76,6 +76,7 @@ mcp = "1.6.0"
python-json-logger = "^3.2.1"
playwright = "^1.51.0"
prompt-toolkit = "^3.0.50"
anyio = "4.9.0"
[tool.poetry.group.dev.dependencies]
ruff = "0.11.7"
+1 -1
View File
@@ -220,7 +220,7 @@ def test_str_replace_multi_line_with_tabs(temp_dir, runtime_cls, run_as_openhand
obs.content
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}:
1\tdef test():
2\t{'\t'.expandtabs()}print("Hello, Universe!")
2\t\tprint("Hello, Universe!")
Review the changes and make sure they are as expected. Edit the file again if necessary."""
)
+77 -2
View File
@@ -3,7 +3,11 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch
from uuid import uuid4
import pytest
from litellm import ContentPolicyViolationError, ContextWindowExceededError
from litellm import (
BadRequestError,
ContentPolicyViolationError,
ContextWindowExceededError,
)
from openhands.controller.agent import Agent
from openhands.controller.agent_controller import AgentController
@@ -499,7 +503,10 @@ async def test_reset_with_pending_action_no_observation(mock_agent, mock_event_s
args, kwargs = mock_event_stream.add_event.call_args
error_obs, source = args
assert isinstance(error_obs, ErrorObservation)
assert error_obs.content == 'The action has not been executed.'
assert (
error_obs.content
== 'The action has not been executed. This may have occurred because the user pressed the stop button, or because the runtime system crashed and restarted due to resource constraints. Any previously established system state, dependencies, or environment variables may have been lost.'
)
assert error_obs.tool_call_metadata == pending_action.tool_call_metadata
assert error_obs._cause == pending_action.id
assert source == EventSource.AGENT
@@ -1486,3 +1493,71 @@ def test_system_message_in_event_stream(mock_agent, test_event_stream):
assert isinstance(events[0], SystemMessageAction)
assert events[0].content == 'Test system message'
assert events[0].tools == ['test_tool']
@pytest.mark.asyncio
async def test_openrouter_context_window_exceeded_error(
mock_agent, test_event_stream, mock_status_callback
):
"""Test that OpenRouter context window exceeded errors are properly detected and handled."""
max_iterations = 5
error_after = 2
class StepState:
def __init__(self):
self.has_errored = False
self.index = 0
self.views = []
def step(self, state: State):
self.views.append(state.view)
# Wait until the right step to throw the error, and make sure we
# only throw it once.
if self.index < error_after or self.has_errored:
self.index += 1
return MessageAction(content=f'Test message {self.index}')
# Create a BadRequestError with the OpenRouter context window exceeded message pattern
error = BadRequestError(
message='litellm.BadRequestError: OpenrouterException - This endpoint\'s maximum context length is 40960 tokens. However, you requested about 42988 tokens (38892 of text input, 4096 in the output). Please reduce the length of either one, or use the "middle-out" transform to compress your prompt automatically.',
model='openrouter/qwen/qwen3-30b-a3b',
llm_provider='openrouter',
)
self.has_errored = True
raise error
step_state = StepState()
mock_agent.step = step_state.step
mock_agent.config = AgentConfig(enable_history_truncation=True)
controller = AgentController(
agent=mock_agent,
event_stream=test_event_stream,
max_iterations=max_iterations,
sid='test',
confirmation_mode=False,
headless_mode=True,
status_callback=mock_status_callback,
)
# Set the agent state to RUNNING
controller.state.agent_state = AgentState.RUNNING
# Run the controller until it hits the error
for _ in range(error_after + 2): # +2 to ensure we go past the error
await controller._step()
if step_state.has_errored:
break
# Verify that the error was handled as a context window exceeded error
# by checking that _handle_long_context_error was called (which adds a CondensationAction)
events = list(test_event_stream.get_events())
condensation_actions = [e for e in events if isinstance(e, CondensationAction)]
# There should be at least one CondensationAction if the error was handled correctly
assert (
len(condensation_actions) > 0
), 'OpenRouter context window exceeded error was not handled correctly'
await controller.close()