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 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. 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 ## Develop inside Docker container
+3 -3
View File
@@ -52,17 +52,17 @@ system requirements and more information.
```bash ```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 \ 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 \ -e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --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)! You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
+1 -1
View File
@@ -391,7 +391,7 @@ type = "noop"
#[llm.condenser] #[llm.condenser]
#model = "gpt-4o" #model = "gpt-4o"
#temperature = 0.1 #temperature = 0.1
#max_tokens = 1024 #max_input_tokens = 1024
#################################### Eval #################################### #################################### Eval ####################################
# Configuration for the evaluation, please refer to the specific evaluation # 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"} - BACKEND_HOST=${BACKEND_HOST:-"0.0.0.0"}
- SANDBOX_API_HOSTNAME=host.docker.internal - 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} - SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports: ports:
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest image: openhands:latest
container_name: openhands-app-${DATE:-} container_name: openhands-app-${DATE:-}
environment: 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 #- 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} - WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports: ports:
@@ -52,7 +52,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -61,7 +61,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 python -m openhands.core.cli
``` ```
@@ -46,7 +46,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -56,6 +56,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 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. La façon la plus simple d'exécuter OpenHands est avec Docker.
```bash ```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 \ 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 \ -e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --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). 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 # ... 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 \ -v /var/run/docker.sock:/var/run/docker.sock \
# ... # ...
``` ```
@@ -34,7 +34,7 @@ Docker で OpenHands を CLI モードで実行するには:
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -44,7 +44,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 python -m openhands.core.cli
``` ```
@@ -31,7 +31,7 @@ DockerでOpenHandsをヘッドレスモードで実行するには:
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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" python -m openhands.core.main -t "write a bash script that prints hi"
``` ```
@@ -13,7 +13,7 @@ OpenHandsがリポジトリで動作する際:
1. リポジトリに`.openhands/microagents/`が存在する場合、そこからリポジトリ固有の指示を読み込みます。 1. リポジトリに`.openhands/microagents/`が存在する場合、そこからリポジトリ固有の指示を読み込みます。
2. 会話のキーワードによってトリガーされる一般的なガイドラインを読み込みます。 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のフォーマット ## 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 ```bash
docker run # ... 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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-v $WORKSPACE_BASE:/opt/workspace_base \ -v $WORKSPACE_BASE:/opt/workspace_base \
@@ -82,5 +82,5 @@ docker network create openhands-network
# 分離されたネットワークで OpenHands を実行 # 分離されたネットワークで OpenHands を実行
docker run # ... \ docker run # ... \
--network openhands-network \ --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 ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 python -m openhands.core.cli
``` ```
@@ -32,7 +32,7 @@ Para executar o OpenHands no modo Headless com Docker:
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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" 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. A maneira mais fácil de executar o OpenHands é no Docker.
```bash ```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 \ 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 \ -e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --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! 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. 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. 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 ## 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. 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 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: Microagentes públicos:
- Monitoram comandos recebidos em busca de suas palavras-chave de acionamento. - 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 - 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. mais exemplos.
@@ -13,7 +13,7 @@ Este é o Runtime padrão que é usado quando você inicia o OpenHands. Você po
``` ```
docker run # ... 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 \ -v /var/run/docker.sock:/var/run/docker.sock \
# ... # ...
``` ```
@@ -50,7 +50,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -59,7 +59,7 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 python -m openhands.core.cli
``` ```
@@ -47,7 +47,7 @@ LLM_API_KEY="sk_test_12345"
```bash ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -57,6 +57,6 @@ docker run -it \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 python -m openhands.core.main -t "write a bash script that prints hi" --no-auto-continue
``` ```
@@ -11,16 +11,16 @@
在 Docker 中运行 OpenHands 是最简单的方式。 在 Docker 中运行 OpenHands 是最简单的方式。
```bash ```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 \ 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 \ -e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --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)。 你也可以在可脚本化的[无头模式](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 # ... 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 \ -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 ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -45,7 +45,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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 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 ```bash
docker run -it \ docker run -it \
--pull=always \ --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 SANDBOX_USER_ID=$(id -u) \
-e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \ -e WORKSPACE_MOUNT_PATH=$WORKSPACE_BASE \
-e LLM_API_KEY=$LLM_API_KEY \ -e LLM_API_KEY=$LLM_API_KEY \
@@ -43,7 +43,7 @@ docker run -it \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \ --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" 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. The easiest way to run OpenHands is in Docker.
```bash ```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 \ 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 \ -e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \ -v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands-state:/.openhands-state \ -v ~/.openhands-state:/.openhands-state \
-p 3000:3000 \ -p 3000:3000 \
--add-host host.docker.internal:host-gateway \ --add-host host.docker.internal:host-gateway \
--name openhands-app \ --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! You'll find OpenHands running at http://localhost:3000!
@@ -46,4 +46,4 @@ Keyword-triggered microagents:
- Apply their specialized knowledge and capabilities. - Apply their specialized knowledge and capabilities.
- Follow defined guidelines and restrictions. - 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 { 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 userEvent from "@testing-library/user-event";
import { AuthModal } from "#/components/features/waitlist/auth-modal"; import { AuthModal } from "#/components/features/waitlist/auth-modal";
import * as CaptureConsent from "#/utils/handle-capture-consent";
import * as AuthHook from "#/context/auth-context"; 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", () => { describe("AuthModal", () => {
beforeAll(() => { beforeEach(() => {
vi.stubGlobal("location", { href: "" }); vi.stubGlobal("location", { href: "" });
vi.spyOn(AuthHook, "useAuth").mockReturnValue({ vi.spyOn(AuthHook, "useAuth").mockReturnValue({
providersAreSet: false, providersAreSet: false,
@@ -16,50 +20,29 @@ describe("AuthModal", () => {
}); });
}); });
afterAll(() => { afterEach(() => {
vi.unstubAllGlobals(); vi.unstubAllGlobals();
vi.restoreAllMocks(); vi.resetAllMocks();
}); });
it("should render a tos checkbox that is unchecked by default", () => { it("should render the GitHub and GitLab buttons", () => {
render(<AuthModal githubAuthUrl={null} appMode="saas" />); render(<AuthModal githubAuthUrl="mock-url" 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");
const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" }); const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" }); const gitlabButton = screen.getByRole("button", { name: "GITLAB$CONNECT_TO_GITLAB" });
expect(githubButton).toBeDisabled(); expect(githubButton).toBeInTheDocument();
expect(gitlabButton).toBeDisabled(); expect(gitlabButton).toBeInTheDocument();
await user.click(checkbox);
expect(githubButton).not.toBeDisabled();
expect(gitlabButton).not.toBeDisabled();
}); });
it("should set user analytics consent to true when the user checks the tos checkbox", async () => { it("should redirect to GitHub auth URL when GitHub button is clicked", async () => {
const handleCaptureConsentSpy = vi.spyOn(
CaptureConsent,
"handleCaptureConsent",
);
const user = userEvent.setup(); 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"); const githubButton = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" });
await user.click(checkbox); await user.click(githubButton);
const button = screen.getByRole("button", { name: "GITHUB$CONNECT_TO_GITHUB" }); expect(window.location.href).toBe(mockUrl);
await user.click(button);
expect(handleCaptureConsentSpy).toHaveBeenCalledWith(true);
}); });
}); });
@@ -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", "name": "openhands-frontend",
"version": "0.34.0", "version": "0.35.0",
"private": true, "private": true,
"type": "module", "type": "module",
"engines": { "engines": {
"node": ">=20.0.0" "node": ">=20.0.0"
}, },
"dependencies": { "dependencies": {
"@heroui/react": "2.7.6", "@heroui/react": "2.7.8",
"@microlink/react-json-view": "^1.26.1", "@microlink/react-json-view": "^1.26.1",
"@monaco-editor/react": "^4.7.0-rc.0", "@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.5.2", "@react-router/node": "^7.5.3",
"@react-router/serve": "^7.5.2", "@react-router/serve": "^7.5.3",
"@react-types/shared": "^3.29.0", "@react-types/shared": "^3.29.0",
"@reduxjs/toolkit": "^2.7.0", "@reduxjs/toolkit": "^2.7.0",
"@stripe/react-stripe-js": "^3.6.0", "@stripe/react-stripe-js": "^3.6.0",
"@stripe/stripe-js": "^7.2.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", "@vitejs/plugin-react": "^4.4.0",
"@xterm/addon-fit": "^0.10.0", "@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0", "@xterm/xterm": "^5.4.0",
@@ -24,14 +24,14 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0", "eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.9.2", "framer-motion": "^12.9.2",
"i18next": "^25.0.1", "i18next": "^25.0.2",
"i18next-browser-languagedetector": "^8.0.5", "i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2", "i18next-http-backend": "^3.0.2",
"isbot": "^5.1.27", "isbot": "^5.1.27",
"jose": "^6.0.10", "jose": "^6.0.10",
"lucide-react": "^0.503.0", "lucide-react": "^0.503.0",
"monaco-editor": "^0.52.2", "monaco-editor": "^0.52.2",
"posthog-js": "^1.236.7", "posthog-js": "^1.237.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-highlight": "^0.15.0", "react-highlight": "^0.15.0",
@@ -40,7 +40,7 @@
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-markdown": "^10.1.0", "react-markdown": "^10.1.0",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
"react-router": "^7.5.2", "react-router": "^7.5.3",
"react-syntax-highlighter": "^15.6.1", "react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9", "react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
@@ -82,7 +82,7 @@
"@babel/types": "^7.27.0", "@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.1.1", "@mswjs/socket.io-binding": "^0.1.1",
"@playwright/test": "^1.52.0", "@playwright/test": "^1.52.0",
"@react-router/dev": "^7.5.2", "@react-router/dev": "^7.5.3",
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.74.7", "@tanstack/eslint-plugin-query": "^5.74.7",
"@testing-library/dom": "^10.4.0", "@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 AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop"; import { ModalBackdrop } from "#/components/shared/modals/modal-backdrop";
import { ModalBody } from "#/components/shared/modals/modal-body"; 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 { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react"; import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react"; import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
@@ -19,7 +17,6 @@ interface AuthModalProps {
export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) { export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [isTosAccepted, setIsTosAccepted] = React.useState(false);
const gitlabAuthUrl = useAuthUrl({ const gitlabAuthUrl = useAuthUrl({
appMode: appMode || null, appMode: appMode || null,
@@ -28,14 +25,14 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
const handleGitHubAuth = () => { const handleGitHubAuth = () => {
if (githubAuthUrl) { if (githubAuthUrl) {
handleCaptureConsent(true); // Always start the OIDC flow, let the backend handle TOS check
window.location.href = githubAuthUrl; window.location.href = githubAuthUrl;
} }
}; };
const handleGitLabAuth = () => { const handleGitLabAuth = () => {
if (gitlabAuthUrl) { if (gitlabAuthUrl) {
handleCaptureConsent(true); // Always start the OIDC flow, let the backend handle TOS check
window.location.href = gitlabAuthUrl; window.location.href = gitlabAuthUrl;
} }
}; };
@@ -50,11 +47,8 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</h1> </h1>
</div> </div>
<TOSCheckbox onChange={() => setIsTosAccepted((prev) => !prev)} />
<div className="flex flex-col gap-3 w-full"> <div className="flex flex-col gap-3 w-full">
<BrandButton <BrandButton
isDisabled={!isTosAccepted}
type="button" type="button"
variant="primary" variant="primary"
onClick={handleGitHubAuth} onClick={handleGitHubAuth}
@@ -65,7 +59,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
</BrandButton> </BrandButton>
<BrandButton <BrandButton
isDisabled={!isTosAccepted}
type="button" type="button"
variant="primary" variant="primary"
onClick={handleGitLabAuth} onClick={handleGitLabAuth}
+19 -5
View File
@@ -13,21 +13,35 @@ import posthog from "posthog-js";
import "./i18n"; import "./i18n";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import store from "./store"; import store from "./store";
import { useConfig } from "./hooks/query/use-config";
import { AuthProvider } from "./context/auth-context"; import { AuthProvider } from "./context/auth-context";
import { queryClientConfig } from "./query-client-config"; import { queryClientConfig } from "./query-client-config";
import OpenHands from "./api/open-hands";
import { displayErrorToast } from "./utils/custom-toast-handlers";
function PosthogInit() { function PosthogInit() {
const { data: config } = useConfig(); const [posthogClientKey, setPosthogClientKey] = React.useState<string | null>(
null,
);
React.useEffect(() => { React.useEffect(() => {
if (config?.POSTHOG_CLIENT_KEY) { (async () => {
posthog.init(config.POSTHOG_CLIENT_KEY, { 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", api_host: "https://us.i.posthog.com",
person_profiles: "identified_only", person_profiles: "identified_only",
}); });
} }
}, [config]); }, [posthogClientKey]);
return null; return null;
} }
+5 -1
View File
@@ -1,14 +1,18 @@
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useConfig } from "./use-config"; import { useConfig } from "./use-config";
import OpenHands from "#/api/open-hands"; import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useBalance = () => { export const useBalance = () => {
const { data: config } = useConfig(); const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
return useQuery({ return useQuery({
queryKey: ["user", "balance"], queryKey: ["user", "balance"],
queryFn: OpenHands.getBalance, queryFn: OpenHands.getBalance,
enabled: 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 { useQuery } from "@tanstack/react-query";
import OpenHands from "#/api/open-hands"; import OpenHands from "#/api/open-hands";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useConfig = () => export const useConfig = () => {
useQuery({ const isOnTosPage = useIsOnTosPage();
return useQuery({
queryKey: ["config"], queryKey: ["config"],
queryFn: OpenHands.getConfig, queryFn: OpenHands.getConfig,
staleTime: 1000 * 60 * 5, // 5 minutes 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 OpenHands from "#/api/open-hands";
import { useConfig } from "./use-config"; import { useConfig } from "./use-config";
import { useAuth } from "#/context/auth-context"; import { useAuth } from "#/context/auth-context";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export const useIsAuthed = () => { export const useIsAuthed = () => {
const { providersAreSet } = useAuth(); const { providersAreSet } = useAuth();
const { data: config } = useConfig(); const { data: config } = useConfig();
const isOnTosPage = useIsOnTosPage();
const appMode = React.useMemo(() => config?.APP_MODE, [config]); const appMode = React.useMemo(() => config?.APP_MODE, [config]);
return useQuery({ return useQuery({
queryKey: ["user", "authenticated", providersAreSet, appMode], queryKey: ["user", "authenticated", providersAreSet, appMode],
queryFn: () => OpenHands.authenticate(appMode!), queryFn: () => OpenHands.authenticate(appMode!),
enabled: !!appMode, enabled: !!appMode && !isOnTosPage,
staleTime: 1000 * 60 * 5, // 5 minutes staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes gcTime: 1000 * 60 * 15, // 15 minutes
retry: false, retry: false,
+4
View File
@@ -4,6 +4,7 @@ import posthog from "posthog-js";
import OpenHands from "#/api/open-hands"; import OpenHands from "#/api/open-hands";
import { useAuth } from "#/context/auth-context"; import { useAuth } from "#/context/auth-context";
import { DEFAULT_SETTINGS } from "#/services/settings"; import { DEFAULT_SETTINGS } from "#/services/settings";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
import { Settings } from "#/types/settings"; import { Settings } from "#/types/settings";
const getSettingsQueryFn = async (): Promise<Settings> => { const getSettingsQueryFn = async (): Promise<Settings> => {
@@ -31,6 +32,8 @@ export const useSettings = () => {
const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } = const { setProviderTokensSet, providerTokensSet, setProvidersAreSet } =
useAuth(); useAuth();
const isOnTosPage = useIsOnTosPage();
const query = useQuery({ const query = useQuery({
queryKey: ["settings", providerTokensSet], queryKey: ["settings", providerTokensSet],
queryFn: getSettingsQueryFn, queryFn: getSettingsQueryFn,
@@ -40,6 +43,7 @@ export const useSettings = () => {
retry: (_, error) => error.status !== 404, retry: (_, error) => error.status !== 404,
staleTime: 1000 * 60 * 5, // 5 minutes staleTime: 1000 * 60 * 5, // 5 minutes
gcTime: 1000 * 60 * 15, // 15 minutes gcTime: 1000 * 60 * 15, // 15 minutes
enabled: !isOnTosPage,
meta: { meta: {
disableToast: true, 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$TOOLS_TAB = "SYSTEM_MESSAGE_MODAL$TOOLS_TAB",
SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS", SYSTEM_MESSAGE_MODAL$PARAMETERS = "SYSTEM_MESSAGE_MODAL$PARAMETERS",
SYSTEM_MESSAGE_MODAL$NO_TOOLS = "SYSTEM_MESSAGE_MODAL$NO_TOOLS", 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", "tr": "belgelendirme",
"de": "Dokumentation" "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": { "DIFF_VIEWER$LOADING": {
"en": "Loading...", "en": "Loading...",
"ja": "読み込み中...", "ja": "読み込み中...",
@@ -6754,4 +6769,53 @@
"es": "No hay herramientas disponibles para este agente", "es": "No hay herramientas disponibles para este agente",
"tr": "Bu ajan için kullanılabilir araç yok" "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 [ export default [
layout("routes/root-layout.tsx", [ layout("routes/root-layout.tsx", [
index("routes/home.tsx"), index("routes/home.tsx"),
route("accept-tos", "routes/accept-tos.tsx"),
route("settings", "routes/settings.tsx", [ route("settings", "routes/settings.tsx", [
index("routes/llm-settings.tsx"), index("routes/llm-settings.tsx"),
route("git", "routes/git-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 { useBalance } from "#/hooks/query/use-balance";
import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal"; import { SetupPaymentModal } from "#/components/features/payment/setup-payment-modal";
import { displaySuccessToast } from "#/utils/custom-toast-handlers"; import { displaySuccessToast } from "#/utils/custom-toast-handlers";
import { useIsOnTosPage } from "#/hooks/use-is-on-tos-page";
export function ErrorBoundary() { export function ErrorBoundary() {
const error = useRouteError(); const error = useRouteError();
@@ -58,6 +59,7 @@ export function ErrorBoundary() {
export default function MainApp() { export default function MainApp() {
const navigate = useNavigate(); const navigate = useNavigate();
const { pathname } = useLocation(); const { pathname } = useLocation();
const tosPageStatus = useIsOnTosPage();
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const { data: settings } = useSettings(); const { data: settings } = useSettings();
const { error, isFetching } = useBalance(); const { error, isFetching } = useBalance();
@@ -71,49 +73,75 @@ export default function MainApp() {
isError: authError, isError: authError,
} = useIsAuthed(); } = useIsAuthed();
// Always call the hook, but we'll only use the result when not on TOS page
const gitHubAuthUrl = useGitHubAuthUrl({ const gitHubAuthUrl = useGitHubAuthUrl({
appMode: config.data?.APP_MODE || null, appMode: config.data?.APP_MODE || null,
gitHubClientId: config.data?.GITHUB_CLIENT_ID || 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); const [consentFormIsOpen, setConsentFormIsOpen] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
if (settings?.LANGUAGE) { // Don't change language when on TOS page
if (!tosPageStatus && settings?.LANGUAGE) {
i18n.changeLanguage(settings.LANGUAGE); i18n.changeLanguage(settings.LANGUAGE);
} }
}, [settings?.LANGUAGE]); }, [settings?.LANGUAGE, tosPageStatus]);
React.useEffect(() => { React.useEffect(() => {
const consentFormModalIsOpen = // Don't show consent form when on TOS page
settings?.USER_CONSENTS_TO_ANALYTICS === null; if (!tosPageStatus) {
const consentFormModalIsOpen =
settings?.USER_CONSENTS_TO_ANALYTICS === null;
setConsentFormIsOpen(consentFormModalIsOpen); 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("/");
} }
}, [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 = const renderAuthModal =
!isFetchingAuth && !userIsAuthed && config.data?.APP_MODE === "saas"; !isFetchingAuth &&
!userIsAuthed &&
!tosPageStatus &&
config.data?.APP_MODE === "saas";
return ( return (
<div <div
@@ -131,7 +159,7 @@ export default function MainApp() {
{renderAuthModal && ( {renderAuthModal && (
<AuthModal <AuthModal
githubAuthUrl={gitHubAuthUrl} githubAuthUrl={effectiveGitHubAuthUrl}
appMode={config.data?.APP_MODE} 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 // Mock requests during tests
beforeAll(() => server.listen({ onUnhandledRequest: "bypass" })); beforeAll(() => server.listen({ onUnhandledRequest: "bypass" }));
afterEach(() => { 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 bug fixes: Create tests to verify issues before implementing fixes
* For new features: Consider test-driven development when appropriate * 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 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 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> </PROBLEM_SOLVING_WORKFLOW>
<SECURITY> <SECURITY>
+8 -1
View File
@@ -76,6 +76,8 @@ from openhands.llm.metrics import Metrics, TokenUsage
TRAFFIC_CONTROL_REMINDER = ( TRAFFIC_CONTROL_REMINDER = (
"Please click on resume button if you'd like to continue, or start a new task." "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: class AgentController:
@@ -566,7 +568,10 @@ class AgentController:
# make a new ErrorObservation with the tool call metadata # make a new ErrorObservation with the tool call metadata
if not found_observation: 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.tool_call_metadata = self._pending_action.tool_call_metadata
obs._cause = self._pending_action.id # type: ignore[attr-defined] obs._cause = self._pending_action.id # type: ignore[attr-defined]
self.event_stream.add_event(obs, EventSource.AGENT) self.event_stream.add_event(obs, EventSource.AGENT)
@@ -842,6 +847,8 @@ class AgentController:
'contextwindowexceedederror' in error_str 'contextwindowexceedederror' in error_str
or 'prompt is too long' in error_str or 'prompt is too long' in error_str
or 'input length and `max_tokens` exceed context limit' 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) or isinstance(e, ContextWindowExceededError)
): ):
if self.agent.config.enable_history_truncation: 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, PROVIDER_TOKEN_TYPE,
WithJsonSchema({'type': 'object', 'additionalProperties': {'type': 'string'}}), 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): class SecretStore(BaseModel):
@@ -67,8 +71,8 @@ class SecretStore(BaseModel):
default_factory=lambda: MappingProxyType({}) default_factory=lambda: MappingProxyType({})
) )
custom_secrets: CUSTOM_SECRETS_TYPE = Field( custom_secrets: CUSTOM_SECRETS_TYPE_WITH_JSON_SCHEMA = Field(
default_factory=lambda: MappingProxyType({}) default_factory=lambda: MappingProxyType({}),
) )
model_config = { model_config = {
+2 -2
View File
@@ -230,8 +230,8 @@ class ConversationMemory:
pending_tool_call_action_messages[llm_response.id] = Message( pending_tool_call_action_messages[llm_response.id] = Message(
role=getattr(assistant_msg, 'role', 'assistant'), role=getattr(assistant_msg, 'role', 'assistant'),
# tool call content SHOULD BE a string # tool call content SHOULD BE a string
content=[TextContent(text=assistant_msg.content or '')] content=[TextContent(text=assistant_msg.content)]
if assistant_msg.content is not None if assistant_msg.content and assistant_msg.content.strip()
else [], else [],
tool_calls=assistant_msg.tool_calls, 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" 1. [Create a personal access token](https://github.com/settings/tokens?type=beta) with read/write scope for "contents", "issues", "pull requests", and "workflows"
- "issues"
- "pull requests"
- "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` Note: If the "Read and write permissions" option is greyed out:
- Select **Read and write permissions** - First check if permissions need to be set at the organization level
- Enable **Allow Github Actions to create and approve pull requests** - 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)
> 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):
5. Set up [GitHub secrets](https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions):
- Required: - Required:
- `LLM_API_KEY`: Your LLM API key - `LLM_API_KEY`: Your LLM API key
- Optional: - Optional:
- `PAT_USERNAME`: GitHub username for the personal access token - `PAT_USERNAME`: GitHub username for the personal access token
- `PAT_TOKEN`: The personal access token - `PAT_TOKEN`: The personal access token
- `LLM_BASE_URL`: Base URL for LLM API (only if using a proxy) - `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 If you prefer to run the resolver programmatically instead of using GitHub Actions, follow these steps:
- The agent will consider all comments in the issue thread when resolving
Using `@openhands-agent` in an issue/pr comment: 1. Install the package:
- Create a new comment containing `@openhands-agent`
- The agent will only consider the comment + comment thread where it's mentioned
## 4. Running Locally
### Installation
```bash ```bash
pip install openhands-ai 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 ```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 GITLAB_TOKEN="your-gitlab-token"
export GIT_USERNAME="your-gitlab-username" export GIT_USERNAME="your-gitlab-username" # Optional, defaults to token owner
# LLM configuration # 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_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 ```bash
python -m openhands.resolver.resolve_issue --selected-repo [OWNER]/[REPO] --issue-number [NUMBER] 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 ```bash
python -m openhands.resolver.send_pull_request --issue-number PR_NUMBER --issue-type pr 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 ```bash
grep '"success":true' output/output.jsonl | sed 's/.*\("number":[0-9]*\).*/\1/g' 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 ```bash
python -m openhands.resolver.visualize_resolver_output --issue-number ISSUE_NUMBER --vis-method json 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 ```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 ## 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). 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 return self.runtime_url
async def connect(self): async def connect(self):
self.log('debug', f'Connecting to remote runtime with session ID: {self.sid}')
try: try:
self.log('debug', 'Starting or attaching to runtime...')
await call_sync_from_async(self._start_or_attach_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.close()
self.log('error', 'Runtime failed to start')
raise raise
self.log('debug', 'Setting up initial environment...')
await call_sync_from_async(self.setup_initial_env) await call_sync_from_async(self.setup_initial_env)
self.log('debug', 'Initial environment setup complete')
self._runtime_initialized = True 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): def _start_or_attach_to_runtime(self):
existing_runtime = self._check_existing_runtime() existing_runtime = self._check_existing_runtime()
@@ -145,17 +152,21 @@ class RemoteRuntime(ActionExecutionClient):
self.send_status_message(' ') self.send_status_message(' ')
def _check_existing_runtime(self) -> bool: def _check_existing_runtime(self) -> bool:
self.log('debug', f'Checking for existing runtime with session ID: {self.sid}')
try: try:
response = self._send_runtime_api_request( response = self._send_runtime_api_request(
'GET', 'GET',
f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}', f'{self.config.sandbox.remote_runtime_api_url}/sessions/{self.sid}',
) )
data = response.json() data = response.json()
self.log('debug', f'Existing runtime check response: {data}')
status = data.get('status') status = data.get('status')
if status == 'running' or status == 'paused': if status == 'running' or status == 'paused':
self.log('debug', f'Found existing runtime with status: {status}')
self._parse_runtime_response(response) self._parse_runtime_response(response)
except httpx.HTTPError as e: except httpx.HTTPError as e:
if e.response.status_code == 404: if e.response.status_code == 404:
self.log('debug', f'No existing runtime found for session ID: {self.sid}')
return False return False
self.log('debug', f'Error while looking for remote runtime: {e}') self.log('debug', f'Error while looking for remote runtime: {e}')
raise raise
@@ -167,6 +178,7 @@ class RemoteRuntime(ActionExecutionClient):
raise raise
if status == 'running': if status == 'running':
self.log('info', f'Using existing running runtime for session ID: {self.sid}')
return True return True
elif status == 'stopped': elif status == 'stopped':
self.log('debug', 'Found existing remote runtime, but it is stopped') self.log('debug', 'Found existing remote runtime, but it is stopped')
@@ -224,16 +236,20 @@ class RemoteRuntime(ActionExecutionClient):
) )
def _start_runtime(self): def _start_runtime(self):
self.log('debug', 'Starting remote runtime...')
# Prepare the request body for the /start endpoint # Prepare the request body for the /start endpoint
command = get_action_execution_server_startup_command( command = get_action_execution_server_startup_command(
server_port=self.port, server_port=self.port,
plugins=self.plugins, plugins=self.plugins,
app_config=self.config, app_config=self.config,
) )
self.log('debug', f'Action execution server command: {command}')
environment = {} environment = {}
if self.config.debug or os.environ.get('DEBUG', 'false').lower() == 'true': if self.config.debug or os.environ.get('DEBUG', 'false').lower() == 'true':
environment['DEBUG'] = 'true' environment['DEBUG'] = 'true'
environment.update(self.config.sandbox.runtime_startup_env_vars) environment.update(self.config.sandbox.runtime_startup_env_vars)
start_request = { start_request = {
'image': self.container_image, 'image': self.container_image,
'command': command, 'command': command,
@@ -242,10 +258,16 @@ class RemoteRuntime(ActionExecutionClient):
'session_id': self.sid, 'session_id': self.sid,
'resource_factor': self.config.sandbox.remote_runtime_resource_factor, 'resource_factor': self.config.sandbox.remote_runtime_resource_factor,
} }
if self.config.sandbox.remote_runtime_class == 'sysbox': if self.config.sandbox.remote_runtime_class == 'sysbox':
start_request['runtime_class'] = 'sysbox-runc' 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' # 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 # Start the sandbox using the /start endpoint
try: try:
response = self._send_runtime_api_request( response = self._send_runtime_api_request(
@@ -255,8 +277,8 @@ class RemoteRuntime(ActionExecutionClient):
) )
self._parse_runtime_response(response) self._parse_runtime_response(response)
self.log( self.log(
'debug', 'info',
f'Runtime started. URL: {self.runtime_url}', f'Runtime started successfully. Runtime ID: {self.runtime_id}, URL: {self.runtime_url}',
) )
except httpx.HTTPError as e: except httpx.HTTPError as e:
self.log('error', f'Unable to start runtime: {str(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 3. Poll for the runtime to be ready
4. Update env vars 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_status_message('STATUS$STARTING_RUNTIME')
self._send_runtime_api_request(
'POST', try:
f'{self.config.sandbox.remote_runtime_api_url}/resume', self.log('debug', 'Sending resume request to runtime API')
json={'runtime_id': self.runtime_id}, self._send_runtime_api_request(
) 'POST',
self._wait_until_alive() f'{self.config.sandbox.remote_runtime_api_url}/resume',
self.setup_initial_env() json={'runtime_id': self.runtime_id},
self.log('debug', 'Runtime resumed.') )
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): def _parse_runtime_response(self, response: httpx.Response):
start_response = response.json() start_response = response.json()
@@ -324,53 +358,62 @@ class RemoteRuntime(ActionExecutionClient):
return retry_decorator(self._wait_until_alive_impl)() return retry_decorator(self._wait_until_alive_impl)()
def _wait_until_alive_impl(self): 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( runtime_info_response = self._send_runtime_api_request(
'GET', 'GET',
f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}', f'{self.config.sandbox.remote_runtime_api_url}/runtime/{self.runtime_id}',
) )
runtime_data = runtime_info_response.json() runtime_data = runtime_info_response.json()
assert 'runtime_id' in runtime_data self.log('debug', f'Runtime info response: {runtime_data}')
assert runtime_data['runtime_id'] == self.runtime_id
assert 'pod_status' in 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() pod_status = runtime_data['pod_status'].lower()
self.log('debug', f'Pod status: {pod_status}') self.log('debug', f'Pod status: {pod_status}')
restart_count = runtime_data.get('restart_count', 0) restart_count = runtime_data.get('restart_count', 0)
if restart_count != 0: if restart_count != 0:
restart_reasons = runtime_data.get('restart_reasons') restart_reasons = runtime_data.get('restart_reasons')
self.log( 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 # FIXME: We should fix it at the backend of /start endpoint, make sure
# the pod is created before returning the response. # the pod is created before returning the response.
# Retry a period of time to give the cluster time to start the pod # Retry a period of time to give the cluster time to start the pod
if pod_status == 'ready': if pod_status == 'ready':
self.log('debug', 'Pod status is ready, checking if action execution server is alive')
try: try:
self.check_if_alive() self.check_if_alive()
self.log('info', 'Runtime is fully alive and ready to accept commands')
return
except httpx.HTTPError as e: except httpx.HTTPError as e:
self.log( self.log(
'warning', '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( raise AgentRuntimeNotReadyError(
f'Runtime /alive failed to respond with 200: {str(e)}' f'Runtime /alive failed to respond with 200: {str(e)}'
) )
return
elif ( elif (
pod_status == 'not found' pod_status == 'not found'
or pod_status == 'pending' or pod_status == 'pending'
or pod_status == 'running' or pod_status == 'running'
): # nb: Running is not yet Ready ): # nb: Running is not yet Ready
self.log('debug', f'Pod is in transitional state: {pod_status}')
raise AgentRuntimeNotReadyError( raise AgentRuntimeNotReadyError(
f'Runtime (ID={self.runtime_id}) is not yet ready. Status: {pod_status}' f'Runtime (ID={self.runtime_id}) is not yet ready. Status: {pod_status}'
) )
elif pod_status in ('failed', 'unknown', 'crashloopbackoff'): elif pod_status in ('failed', 'unknown', 'crashloopbackoff'):
if pod_status == 'crashloopbackoff': if pod_status == 'crashloopbackoff':
self.log('error', f'Pod is in CrashLoopBackOff state. Restart count: {restart_count}')
raise AgentRuntimeUnavailableError( raise AgentRuntimeUnavailableError(
'Runtime crashed and is being restarted, potentially due to memory usage. Please try again.' 'Runtime crashed and is being restarted, potentially due to memory usage. Please try again.'
) )
else: else:
self.log('error', f'Pod is in failed state: {pod_status}')
raise AgentRuntimeUnavailableError( raise AgentRuntimeUnavailableError(
f'Runtime is unavailable (status: {pod_status}). Please try again.' f'Runtime is unavailable (status: {pod_status}). Please try again.'
) )
@@ -385,11 +428,18 @@ class RemoteRuntime(ActionExecutionClient):
raise AgentRuntimeNotReadyError() raise AgentRuntimeNotReadyError()
def close(self): def close(self):
self.log('debug', f'Closing remote runtime with ID: {self.runtime_id}')
if self.attach_to_existing: if self.attach_to_existing:
self.log('debug', 'Runtime was attached to existing instance, not stopping it')
super().close() super().close()
return return
if self.config.sandbox.keep_runtime_alive: 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: if self.config.sandbox.pause_closed_runtimes:
self.log('debug', 'pause_closed_runtimes is set, pausing the runtime')
try: try:
if not self._runtime_closed: if not self._runtime_closed:
self._send_runtime_api_request( self._send_runtime_api_request(
@@ -397,12 +447,14 @@ class RemoteRuntime(ActionExecutionClient):
f'{self.config.sandbox.remote_runtime_api_url}/pause', f'{self.config.sandbox.remote_runtime_api_url}/pause',
json={'runtime_id': self.runtime_id}, 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: except Exception as e:
self.log('error', f'Unable to pause runtime: {str(e)}') self.log('error', f'Unable to pause runtime: {str(e)}')
raise e raise e
super().close() super().close()
return return
self.log('debug', 'Stopping the runtime')
try: try:
if not self._runtime_closed: if not self._runtime_closed:
self._send_runtime_api_request( self._send_runtime_api_request(
@@ -410,7 +462,7 @@ class RemoteRuntime(ActionExecutionClient):
f'{self.config.sandbox.remote_runtime_api_url}/stop', f'{self.config.sandbox.remote_runtime_api_url}/stop',
json={'runtime_id': self.runtime_id}, 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: except Exception as e:
self.log('error', f'Unable to stop runtime: {str(e)}') self.log('error', f'Unable to stop runtime: {str(e)}')
raise e raise e
@@ -418,15 +470,21 @@ class RemoteRuntime(ActionExecutionClient):
super().close() super().close()
def _send_runtime_api_request(self, method, url, **kwargs): def _send_runtime_api_request(self, method, url, **kwargs):
self.log('debug', f'Sending {method} request to {url} with kwargs: {kwargs}')
try: try:
kwargs['timeout'] = self.config.sandbox.remote_runtime_api_timeout 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: except httpx.TimeoutException:
self.log( self.log(
'error', 'error',
f'No response received within the timeout period for url: {url}', f'No response received within the timeout period for url: {url}',
) )
raise 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): def _send_action_server_request(self, method, url, **kwargs):
if not self.config.sandbox.remote_runtime_enable_retries: 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): async def connect(connection_id: str, environ):
logger.info(f'sio:connect: {connection_id}') logger.info(f'sio:connect: {connection_id}')
query_params = parse_qs(environ.get('QUERY_STRING', '')) 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] conversation_id = query_params.get('conversation_id', [None])[0]
raw_list = query_params.get('providers_set', []) raw_list = query_params.get('providers_set', [])
providers_list = [] providers_list = []
Generated
+29 -29
View File
@@ -207,14 +207,14 @@ vertex = ["google-auth[requests] (>=2,<3)"]
[[package]] [[package]]
name = "anyio" name = "anyio"
version = "4.8.0" version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations" description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "evaluation", "runtime", "test"] groups = ["main", "evaluation", "runtime", "test"]
files = [ files = [
{file = "anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a"}, {file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a"}, {file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
] ]
[package.dependencies] [package.dependencies]
@@ -223,8 +223,8 @@ sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras] [package.extras]
doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] doc = ["Sphinx (>=8.2,<9.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\""] 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)"] trio = ["trio (>=0.26.1)"]
[[package]] [[package]]
@@ -496,18 +496,18 @@ files = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.38.3" version = "1.38.4"
description = "The AWS SDK for Python" description = "The AWS SDK for Python"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "boto3-1.38.3-py3-none-any.whl", hash = "sha256:9218f86e2164e1bddb75d435bbde4fa651aa58687213d7e3e1b50f7eb8868f66"}, {file = "boto3-1.38.4-py3-none-any.whl", hash = "sha256:ab315d38409f5b3262b653a10b0fac786bcff7e51e03dcb99ff38ba16bf85630"},
{file = "boto3-1.38.3.tar.gz", hash = "sha256:655d51abcd68a40a33c52dbaa2ca73fc63c746b894e2ae22ed8ddc1912ddd93f"}, {file = "boto3-1.38.4.tar.gz", hash = "sha256:4990df0087fe7be944ba06c2d1e6512b5a24f821af5a4881f24309e13ae29e68"},
] ]
[package.dependencies] [package.dependencies]
botocore = ">=1.38.3,<1.39.0" botocore = ">=1.38.4,<1.39.0"
jmespath = ">=0.7.1,<2.0.0" jmespath = ">=0.7.1,<2.0.0"
s3transfer = ">=0.12.0,<0.13.0" s3transfer = ">=0.12.0,<0.13.0"
@@ -516,14 +516,14 @@ crt = ["botocore[crt] (>=1.21.0,<2.0a0)"]
[[package]] [[package]]
name = "boto3-stubs" name = "boto3-stubs"
version = "1.38.3" version = "1.38.4"
description = "Type annotations for boto3 1.38.3 generated with mypy-boto3-builder 8.10.1" description = "Type annotations for boto3 1.38.4 generated with mypy-boto3-builder 8.10.1"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
groups = ["evaluation"] groups = ["evaluation"]
files = [ files = [
{file = "boto3_stubs-1.38.3-py3-none-any.whl", hash = "sha256:93a2c38987dd0ee19a661e8fd9a77fb4b4a30e56f63115701c307bfc55e2695c"}, {file = "boto3_stubs-1.38.4-py3-none-any.whl", hash = "sha256:ae931b5f40ebf70cd7ddc36065d058d406c6aaecc766bd843be855487fc6345e"},
{file = "boto3_stubs-1.38.3.tar.gz", hash = "sha256:e406626de8daf537984678355ad0e32d838865c4ea3d223268964d4e6fb44534"}, {file = "boto3_stubs-1.38.4.tar.gz", hash = "sha256:eff7f741a9b9df5b0af1e7018878a56028f05bb3d9c57cd50cc4f60bdae7960b"},
] ]
[package.dependencies] [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)"] bedrock-runtime = ["mypy-boto3-bedrock-runtime (>=1.38.0,<1.39.0)"]
billing = ["mypy-boto3-billing (>=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)"] 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)"] braket = ["mypy-boto3-braket (>=1.38.0,<1.39.0)"]
budgets = ["mypy-boto3-budgets (>=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)"] 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]] [[package]]
name = "botocore" name = "botocore"
version = "1.38.3" version = "1.38.4"
description = "Low-level, data-driven core of boto 3." description = "Low-level, data-driven core of boto 3."
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "botocore-1.38.3-py3-none-any.whl", hash = "sha256:96f823240fe3704b99c17d1d1b2fd2d1679cf56d2a55b095f00255b76087cbf0"}, {file = "botocore-1.38.4-py3-none-any.whl", hash = "sha256:6206cf07be1069efaead2ddc858eb752dafef276ebbe88ac32b5c427b1d90570"},
{file = "botocore-1.38.3.tar.gz", hash = "sha256:790f8f966201781f5fcf486d48b4492e9f734446bbf9d19ef8159d08be854243"}, {file = "botocore-1.38.4.tar.gz", hash = "sha256:6143546bb56f1da4dff8d285cb6a3b8b0b6442451fe5937cb48a62bf7275386f"},
] ]
[package.dependencies] [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\""} grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [ proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, {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" 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" 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" grpc-google-iam-v1 = ">=0.14.0,<1.0.0dev"
proto-plus = [ proto-plus = [
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, {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" 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]] [[package]]
name = "json-repair" name = "json-repair"
version = "0.43.0" version = "0.44.0"
description = "A package to repair broken json strings" description = "A package to repair broken json strings"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "json_repair-0.43.0-py3-none-any.whl", hash = "sha256:3f2b66819c9f5e29edd5dd4851223b72d10ed816b6423b3c92e424090c3ffc1d"}, {file = "json_repair-0.44.0-py3-none-any.whl", hash = "sha256:4646b59decf8763c20cd218079a12e334202d658d760ddd999f23812f07bb9cd"},
{file = "json_repair-0.43.0.tar.gz", hash = "sha256:77cc6eda6f407ff5fe9544f962e42b332cca1e8c9f3f9f9dc660327028e0d651"}, {file = "json_repair-0.44.0.tar.gz", hash = "sha256:732469762471e535de8aadcb4ec5a4b47e96c677d036cbc1c78ab396925c6a71"},
] ]
[[package]] [[package]]
@@ -4882,14 +4882,14 @@ files = [
[[package]] [[package]]
name = "modal" name = "modal"
version = "0.74.30" version = "0.74.35"
description = "Python client library for Modal" description = "Python client library for Modal"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
groups = ["main", "evaluation"] groups = ["main", "evaluation"]
files = [ files = [
{file = "modal-0.74.30-py3-none-any.whl", hash = "sha256:46006cb57309171fe36ee41528a7cc8c0e67c88afd9bf04a9900313c18925aa4"}, {file = "modal-0.74.35-py3-none-any.whl", hash = "sha256:845c3176e5fc2d0856ff1d88a5fc7699552e6542135aedae6cd52598faa55fc0"},
{file = "modal-0.74.30.tar.gz", hash = "sha256:14bd2ea0ebc9ab1ebce29ea76ddf12047f23599983725c5f82990ae97bea05c7"}, {file = "modal-0.74.35.tar.gz", hash = "sha256:bef2a40f18a40514e7502dbe543fc026b0a2542597669cd0bc0fa0db1149600a"},
] ]
[package.dependencies] [package.dependencies]
@@ -5633,14 +5633,14 @@ voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"]
[[package]] [[package]]
name = "openhands-aci" name = "openhands-aci"
version = "0.2.10" version = "0.2.11"
description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands."
optional = false optional = false
python-versions = "<4.0,>=3.12" python-versions = "<4.0,>=3.12"
groups = ["main"] groups = ["main"]
files = [ files = [
{file = "openhands_aci-0.2.10-py3-none-any.whl", hash = "sha256:0703eb117e24326d80d990b81a0c71e28a364f56999095b95c146c934e40fc55"}, {file = "openhands_aci-0.2.11-py3-none-any.whl", hash = "sha256:bb6cdd30da8cf57292f7d1d87b2396a31af3f3abc2f5d6f02c22cb648956b41e"},
{file = "openhands_aci-0.2.10.tar.gz", hash = "sha256:a5e6bf46cbd9a99c5c592548419a9a3b3091c4e82f0227e8aaf470b18a261cc9"}, {file = "openhands_aci-0.2.11.tar.gz", hash = "sha256:38ae6abbeedfc0f6018dc5db58ad0ccc38295d649fe27218a0acab689df0e6c7"},
] ]
[package.dependencies] [package.dependencies]
@@ -10269,4 +10269,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = "^3.12" python-versions = "^3.12"
content-hash = "d3f933e9abf6be481ec137e14f8f7ac502afd591a9ba74b315737fd894ca5cfe" content-hash = "e8326a1441d5ce74c017755566e1e0d865551712290c00202d257c931b7dc5bd"
+3 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "openhands-ai" name = "openhands-ai"
version = "0.34.0" version = "0.35.0"
description = "OpenHands: Code Less, Make More" description = "OpenHands: Code Less, Make More"
authors = ["OpenHands"] authors = ["OpenHands"]
license = "MIT" license = "MIT"
@@ -62,7 +62,7 @@ runloop-api-client = "0.32.0"
libtmux = ">=0.37,<0.40" libtmux = ">=0.37,<0.40"
pygithub = "^2.5.0" pygithub = "^2.5.0"
joblib = "*" joblib = "*"
openhands-aci = "^0.2.10" openhands-aci = "0.2.11"
python-socketio = "^5.11.4" python-socketio = "^5.11.4"
redis = "^5.2.0" redis = "^5.2.0"
sse-starlette = "^2.1.3" sse-starlette = "^2.1.3"
@@ -76,6 +76,7 @@ mcp = "1.6.0"
python-json-logger = "^3.2.1" python-json-logger = "^3.2.1"
playwright = "^1.51.0" playwright = "^1.51.0"
prompt-toolkit = "^3.0.50" prompt-toolkit = "^3.0.50"
anyio = "4.9.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
ruff = "0.11.7" 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 obs.content
== f"""The file {test_file} has been edited. Here's the result of running `cat -n` on a snippet of {test_file}: == 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(): 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.""" 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 from uuid import uuid4
import pytest import pytest
from litellm import ContentPolicyViolationError, ContextWindowExceededError from litellm import (
BadRequestError,
ContentPolicyViolationError,
ContextWindowExceededError,
)
from openhands.controller.agent import Agent from openhands.controller.agent import Agent
from openhands.controller.agent_controller import AgentController 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 args, kwargs = mock_event_stream.add_event.call_args
error_obs, source = args error_obs, source = args
assert isinstance(error_obs, ErrorObservation) 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.tool_call_metadata == pending_action.tool_call_metadata
assert error_obs._cause == pending_action.id assert error_obs._cause == pending_action.id
assert source == EventSource.AGENT 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 isinstance(events[0], SystemMessageAction)
assert events[0].content == 'Test system message' assert events[0].content == 'Test system message'
assert events[0].tools == ['test_tool'] 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()