Compare commits

..

1 Commits

Author SHA1 Message Date
mamoodi
8478fbeffd Release 0.46.0 2025-06-24 13:52:08 -04:00
156 changed files with 2146 additions and 4346 deletions

View File

@@ -40,7 +40,9 @@ jobs:
# Only build nikolaik on PRs, otherwise build both nikolaik and ubuntu.
if [[ "$GITHUB_EVENT_NAME" == "pull_request" ]]; then
json=$(jq -n -c '[
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" }
{ image: "nikolaik/python-nodejs:python3.12-nodejs22", tag: "nikolaik" },
{ image: "ubuntu:24.04", tag: "ubuntu" }
]')
else
json=$(jq -n -c '[

View File

@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.47-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.46-nikolaik`
## Develop inside Docker container

View File

@@ -11,7 +11,7 @@
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Join our Slack community"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Join our Discord community"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="Credits"></a>
<br/>
@@ -62,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
docker.all-hands.dev/all-hands-ai/openhands:0.46
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
@@ -117,7 +117,7 @@ troubleshooting resources, and advanced configuration options.
OpenHands is a community-driven project, and we welcome contributions from everyone. We do most of our communication
through Slack, so this is the best place to start, but we also are happy to have you contact us on Discord or Github:
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - Here we talk about research, architecture, and future development.
- [Join our Slack workspace](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - Here we talk about research, architecture, and future development.
- [Join our Discord server](https://discord.gg/ESHStjSjD4) - This is a community-run server for general discussion, questions, and feedback.
- [Read or post Github Issues](https://github.com/All-Hands-AI/OpenHands/issues) - Check out the issues we're working on, or add your own ideas.

View File

@@ -12,7 +12,7 @@
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="加入我们的Slack社区"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="加入我们的Discord社区"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="致谢"></a>
<br/>
@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
docker.all-hands.dev/all-hands-ai/openhands:0.46
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。
@@ -107,7 +107,7 @@ docker run -it --rm --pull=always \
OpenHands是一个社区驱动的项目我们欢迎每个人的贡献。我们大部分沟通
通过Slack进行因此这是开始的最佳场所但我们也很乐意您通过Discord或Github与我们联系
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Slack工作空间](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) - 这里我们讨论研究、架构和未来发展。
- [加入我们的Discord服务器](https://discord.gg/ESHStjSjD4) - 这是一个社区运营的服务器,用于一般讨论、问题和反馈。
- [阅读或发布Github问题](https://github.com/All-Hands-AI/OpenHands/issues) - 查看我们正在处理的问题,或添加您自己的想法。

View File

@@ -10,7 +10,7 @@
<a href="https://github.com/All-Hands-AI/OpenHands/stargazers"><img src="https://img.shields.io/github/stars/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="Stargazers"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/LICENSE"><img src="https://img.shields.io/github/license/All-Hands-AI/OpenHands?style=for-the-badge&color=blue" alt="MIT License"></a>
<br/>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
<a href="https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A"><img src="https://img.shields.io/badge/Slack-Join%20Us-red?logo=slack&logoColor=white&style=for-the-badge" alt="Slackコミュニティに参加"></a>
<a href="https://discord.gg/ESHStjSjD4"><img src="https://img.shields.io/badge/Discord-Join%20Us-purple?logo=discord&logoColor=white&style=for-the-badge" alt="Discordコミュニティに参加"></a>
<a href="https://github.com/All-Hands-AI/OpenHands/blob/main/CREDITS.md"><img src="https://img.shields.io/badge/Project-Credits-blue?style=for-the-badge&color=FFE165&logo=github&logoColor=white" alt="クレジット"></a>
<br/>
@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
docker.all-hands.dev/all-hands-ai/openhands:0.46
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -1,156 +0,0 @@
# Resolver Runtime Refactoring Plan
## Task Overview
Refactor the resolver component to reuse setup.py functions for runtime initialization, connection, and completion instead of reinventing the wheel.
## Repository Cloning Patterns Analysis
### Repository Cloning Patterns Across OpenHands Entry Points
#### 1. **Resolver (issue_resolver.py)** - DIFFERENT PATTERN (Legacy)
```python
# Step 1: Clone to separate location
subprocess.check_output(['git', 'clone', url, f'{output_dir}/repo'])
# Step 2: Later, copy repo to workspace
shutil.copytree(os.path.join(self.output_dir, 'repo'), self.workspace_base)
# Step 3: Create and connect runtime
runtime = create_runtime(config)
await runtime.connect()
# Step 4: Initialize runtime (git config, setup scripts)
self.initialize_runtime(runtime)
```
#### 2. **Main.py** - STANDARD PATTERN
```python
# Step 1: Create and connect runtime
runtime = create_runtime(config)
await runtime.connect()
# Step 2: Clone directly into runtime workspace + setup
repo_directory = initialize_repository_for_runtime(runtime, selected_repository)
```
#### 3. **Server/Session** - STANDARD PATTERN
```python
# Step 1: Create and connect runtime
# Step 2: Clone directly into runtime workspace
await runtime.clone_or_init_repo(tokens, repo, branch)
# Step 3: Run setup scripts
await runtime.maybe_run_setup_script()
await runtime.maybe_setup_git_hooks()
```
#### 4. **Setup.py's initialize_repository_for_runtime()** - STANDARD PATTERN
```python
# Calls runtime.clone_or_init_repo() + setup scripts
repo_directory = runtime.clone_or_init_repo(tokens, repo, branch)
runtime.maybe_run_setup_script()
runtime.maybe_setup_git_hooks()
```
### The Issue
The **resolver is the odd one out** - it uses a 2-step process (clone to temp location, then copy to workspace) due to **legacy reasons** (it was originally developed as a separate app built on OH, not a component of OH). All other entry points use the standard pattern (clone directly into runtime workspace).
## Current State Analysis
### ✅ What Resolver Already Does Right:
- [x] Uses `create_runtime()` from setup.py for runtime creation
### ❌ What Needs to be Fixed:
- [ ] **Resolver uses legacy 2-step cloning instead of standard runtime.clone_or_init_repo()**
- [ ] Resolver has custom `initialize_runtime()` method that duplicates setup.py logic
- [ ] Resolver has custom `complete_runtime()` method with no setup.py equivalent
- [ ] Resolver doesn't follow proper runtime cleanup patterns like main.py
- [ ] Runtime connection pattern is inconsistent across codebase
## Refactoring Steps
### Phase 1: Fix Repository Cloning Pattern (PRIORITY)
**Goal**: Make resolver use the same repository cloning pattern as all other OpenHands entry points.
- [ ] **Step 1.1**: Replace resolver's legacy 2-step cloning with standard pattern
- Remove `subprocess.check_output(['git', 'clone', ...])` from `resolve_issue()`
- Remove `shutil.copytree()` from `process_issue()`
- Use `initialize_repository_for_runtime()` instead
- This will clone directly into runtime workspace AND run setup scripts
- [ ] **Step 1.2**: Update resolver workflow to match standard pattern
- Create and connect runtime first
- Then call `initialize_repository_for_runtime()` for cloning + setup
- Remove the manual repo copying step entirely
- Ensure base_commit is still captured correctly
### Phase 2: Refactor Runtime Initialization and Completion
**Goal**: Remove code duplication between resolver and setup.py for runtime operations.
- [ ] **Step 2.1**: Create missing functions in setup.py
- Create `setup_runtime_environment()` for git config and platform-specific setup
- Create `complete_runtime_session()` for git patch generation
- Create `cleanup_runtime()` for proper resource cleanup
- [ ] **Step 2.2**: Replace resolver's `initialize_runtime()`
- Use setup.py's `setup_runtime_environment()` instead
- Remove duplicate git configuration code
- Maintain platform-specific behavior (GitLab CI)
- [ ] **Step 2.3**: Replace resolver's `complete_runtime()`
- Use setup.py's `complete_runtime_session()` instead
- Move git patch generation logic to setup.py
- Ensure return values match resolver's expectations
- [ ] **Step 2.4**: Add proper runtime cleanup to resolver
- Use setup.py's `cleanup_runtime()` function
- Ensure resources are properly released in try/finally blocks
### Phase 3: Testing and Validation
- [ ] **Step 3.1**: Test resolver functionality with refactored code
- Verify git operations work correctly
- Verify setup scripts are executed
- Verify git hooks are set up
- [ ] **Step 3.2**: Test runtime lifecycle (create → connect → clone → initialize → complete → cleanup)
- Ensure no resource leaks
- Verify proper error handling
- [ ] **Step 3.3**: Verify resolver output remains consistent
- Git patches are generated correctly
- Issue resolution works as before
- No regression in functionality
### Phase 4: Code Quality and Documentation
- [ ] **Step 4.1**: Add proper documentation to new setup.py functions
- Document parameters and return values
- Add usage examples
- Document platform-specific behavior
- [ ] **Step 4.2**: Remove obsolete code from resolver
- Delete old `initialize_runtime()` method
- Delete old `complete_runtime()` method
- Clean up imports and unused code
- [ ] **Step 4.3**: Update any other components that might benefit from these functions
- Check if other entry points could use the same patterns
- Ensure consistency across the codebase
## Success Criteria
- [ ] **Resolver uses standard repository cloning pattern (runtime.clone_or_init_repo)**
- [ ] Resolver uses setup.py functions for all runtime operations
- [ ] No code duplication between resolver and setup.py
- [ ] Proper runtime lifecycle management (connect → initialize → complete → cleanup)
- [ ] All existing resolver functionality preserved
- [ ] Consistent patterns across all OpenHands entry points
- [ ] Proper error handling and resource cleanup
## Files to Modify
1. `/openhands/core/setup.py` - Add new runtime management functions
2. `/openhands/resolver/issue_resolver.py` - Refactor to use setup.py functions
3. Any tests related to resolver functionality
## Risk Mitigation
- Maintain backward compatibility during refactoring
- Test thoroughly before removing old code
- Keep git patch generation logic identical to avoid breaking issue resolution
- Ensure platform-specific behavior (GitLab CI) is preserved

View File

@@ -10,7 +10,18 @@
# General core configurations
##############################################################################
[core]
# API keys and configuration for core services
# API key for E2B
#e2b_api_key = ""
# API key for Modal
#modal_api_token_id = ""
#modal_api_token_secret = ""
# API key for Daytona
#daytona_api_key = ""
# Daytona Target
#daytona_target = ""
# Base path for the workspace
#workspace_base = "./workspace"
@@ -260,9 +271,6 @@ enable_finish = true
# length limit
enable_history_truncation = true
# Whether the condensation request tool is enabled
enable_condensation_request = false
[agent.RepoExplorerAgent]
# Example: use a cheaper model for RepoExplorerAgent to reduce cost, especially
# useful when an agent doesn't demand high quality but uses a lot of tokens

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.47-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.46-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -3,9 +3,9 @@ repos:
rev: v5.0.0
hooks:
- id: trailing-whitespace
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
exclude: ^(docs/|modules/|python/|openhands-ui/)
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/)
exclude: ^(docs/|modules/|python/|openhands-ui/)
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements
@@ -28,19 +28,17 @@ repos:
entry: ruff check --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
args: [--fix, --unsafe-fixes]
exclude: third_party/
# Run the formatter.
- id: ruff-format
entry: ruff format --config dev_config/python/ruff.toml
types_or: [python, pyi, jupyter]
exclude: third_party/
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.15.0
hooks:
- id: mypy
additional_dependencies:
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, pydantic, lxml]
[types-requests, types-setuptools, types-pyyaml, types-toml, types-docker, lxml]
# To see gaps add `--html-report mypy-report/`
entry: mypy --config-file dev_config/python/mypy.ini openhands/
always_run: true

View File

@@ -7,9 +7,3 @@ warn_unreachable = True
warn_redundant_casts = True
no_implicit_optional = True
strict_optional = True
# Exclude third-party runtime directory from type checking
exclude = third_party/
[mypy-openhands.memory.condenser.impl.*]
disable_error_code = override

View File

@@ -1,6 +1,3 @@
# Exclude third-party runtime directory from linting
exclude = ["third_party/"]
[lint]
select = [
"E",

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.46-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 for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -196,7 +196,7 @@
},
"footer": {
"socials": {
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA",
"slack": "https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A",
"github": "https://github.com/All-Hands-AI/OpenHands",
"discord": "https://discord.gg/ESHStjSjD4"
}

View File

@@ -3,15 +3,6 @@ title: Slack Integration (Beta)
description: This guide walks you through installing the OpenHands Slack app.
---
<iframe
className="w-full aspect-video"
src="https://www.youtube.com/embed/hbloGmfZsJ4"
title="OpenHands Slack Integration Tutorial"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen>
</iframe>
## Prerequisites
- Access to OpenHands Cloud.

View File

@@ -12,6 +12,22 @@ description: This page outlines all available configuration options for OpenHand
The core configuration options are defined in the `[core]` section of the `config.toml` file.
### API Keys
- `e2b_api_key`
- Type: `str`
- Default: `""`
- Description: API key for E2B
- `modal_api_token_id`
- Type: `str`
- Default: `""`
- Description: API token ID for Modal
- `modal_api_token_secret`
- Type: `str`
- Default: `""`
- Description: API token secret for Modal
### Workspace
- `workspace_base` **(Deprecated)**
- Type: `str`

View File

@@ -88,7 +88,7 @@ If you would like to set things up more systematically, you can:
1. **Search existing issues**: Check our [GitHub issues](https://github.com/All-Hands-AI/OpenHands/issues) to see if
others have encountered the same problem.
2. **Join our community**: Get help from other users and developers:
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA)
- [Slack community](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A)
- [Discord server](https://discord.gg/ESHStjSjD4)
3. **Check our troubleshooting guide**: Common issues and solutions are documented in
[Troubleshooting](/usage/troubleshooting/troubleshooting).

View File

@@ -7,15 +7,6 @@ description: The Command-Line Interface (CLI) provides a powerful interface that
This mode is different from the [headless mode](/usage/how-to/headless-mode), which is non-interactive and better
for scripting.
<iframe
className="w-full aspect-video"
src="https://www.youtube.com/embed/PfvIx4y8h7w"
title="OpenHands CLI Tutorial"
frameBorder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen>
</iframe>
## Getting Started
### Running with Python
@@ -64,7 +55,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +64,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
docker.all-hands.dev/all-hands-ai/openhands:0.46 \
python -m openhands.cli.main --override-cli-mode true
```

View File

@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
docker.all-hands.dev/all-hands-ai/openhands:0.46 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
docker.all-hands.dev/all-hands-ai/openhands:0.46
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.47
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.46
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None
@@ -119,7 +119,7 @@ When started for the first time, OpenHands will prompt you to set up the LLM pro
That's it! You can now start using OpenHands with the local LLM server.
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-3847of6xi-xuYJIPa6YIPg4ElbDWbtSA) or [Discord](https://discord.gg/ESHStjSjD4).
If you encounter any issues, let us know on [Slack](https://join.slack.com/t/openhands-ai/shared_invite/zt-34zm4j0gj-Qz5kRHoca8DFCbqXPS~f_A) or [Discord](https://discord.gg/ESHStjSjD4).
## Advanced: Alternative LLM Backends

View File

@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.47
docker.all-hands.dev/all-hands-ai/openhands:0.46
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.

View File

@@ -11,8 +11,6 @@ accordingly. However, they are applied to all repositories belonging to the orga
Add a `.openhands` repository under the organization or user and create a `microagents` directory and place the
microagents in that directory.
For GitLab organizations, use `openhands-config` as the repository name instead of `.openhands`, since GitLab doesn't support repository names starting with non-alphanumeric characters.
## Example
General microagent file example for organization `Great-Co` located inside the `.openhands` repository:
@@ -22,5 +20,3 @@ General microagent file example for organization `Great-Co` located inside the `
* Document interfaces and public APIs; use implementation comments only for non-obvious logic.
* Follow the same naming convention for variables, classes, constants, etc. already used in each repository.
```
For GitLab organizations, the same microagent would be located inside the `openhands-config` repository.

View File

@@ -9,6 +9,8 @@ commands.
By default, OpenHands uses a [Docker-based runtime](/usage/runtimes/docker), running on your local computer.
This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM.
We also support other runtimes, which are typically managed by third-parties.
Additionally, we provide a [Local Runtime](/usage/runtimes/local) that runs directly on your machine without Docker,
which can be useful in controlled environments like CI pipelines.
@@ -19,18 +21,6 @@ OpenHands supports several different runtime environments:
- [Docker Runtime](/usage/runtimes/docker) - The default runtime that uses Docker containers for isolation (recommended for most users).
- [OpenHands Remote Runtime](/usage/runtimes/remote) - Cloud-based runtime for parallel execution (beta).
- [Local Runtime](/usage/runtimes/local) - Direct execution on your local machine without Docker.
### Third-Party Runtimes
The following third-party runtimes are available when you install the `third_party_runtimes` extra:
```bash
pip install openhands-ai[third_party_runtimes]
```
- [E2B Runtime](/usage/runtimes/e2b) - Open source runtime using E2B sandboxes.
- [Modal Runtime](/usage/runtimes/modal) - Serverless runtime using Modal infrastructure.
- [Runloop Runtime](/usage/runtimes/runloop) - Cloud runtime using Runloop infrastructure.
- [Daytona Runtime](/usage/runtimes/daytona) - Development environment runtime using Daytona.
**Note**: These third-party runtimes are supported by their respective developers, not by the OpenHands team. For issues specific to these runtimes, please refer to their documentation or contact their support teams.
- And more third-party runtimes:
- [Modal Runtime](/usage/runtimes/modal) - Runtime provided by our partners at Modal.
- [Daytona Runtime](/usage/runtimes/daytona) - Runtime provided by Daytona.

View File

@@ -3,20 +3,13 @@ import copy
import functools
import os
import re
import shutil
import zipfile
import huggingface_hub
import pandas as pd
from datasets import load_dataset
from PIL import Image
from pydantic import SecretStr
from evaluation.benchmarks.gaia.scorer import question_scorer
from evaluation.benchmarks.gaia.utils import (
image_to_jpg_base64_url,
image_to_png_base64_url,
)
from evaluation.utils.shared import (
EvalMetadata,
EvalOutput,
@@ -104,44 +97,27 @@ def initialize_runtime(
if instance['file_name'] != '':
# if this question comes with a file, we need to save it to the workspace
assert metadata.data_split is not None
extension_name = instance['file_name'].split('.')[-1]
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
assert os.path.exists(src_file)
if extension_name == 'zip':
temp_dir = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, 'tmp_file'
)
os.makedirs(temp_dir, exist_ok=True)
with zipfile.ZipFile(src_file, 'r') as zip_ref:
zip_ref.extractall(temp_dir)
for root, dirs, files in os.walk(temp_dir):
for file in files:
dest_file = '/workspace'
runtime.copy_to(os.path.join(root, file), dest_file)
shutil.rmtree(temp_dir)
elif extension_name not in ['jpg', 'png']:
dest_file = '/workspace'
runtime.copy_to(src_file, dest_file)
dest_file = os.path.join('/workspace', instance['file_name'])
runtime.copy_to(src_file, dest_file)
# rename to file.extension_name
action = CmdRunAction(
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
# rename to file.extension_name
extension_name = instance['file_name'].split('.')[-1]
action = CmdRunAction(
command=f'mv /workspace/{instance["file_name"]} /workspace/file.{extension_name}'
)
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(command='cd /workspace')
logger.info(action, extra={'msg_type': 'ACTION'})
obs = runtime.run_action(action)
assert obs.exit_code == 0
action = CmdRunAction(
command='apt-get update && apt-get install -y ffmpeg && apt-get install -y ffprobe'
)
runtime.run_action(action)
logger.info(f'{"-" * 50} END Runtime Initialization Fn {"-" * 50}')
@@ -175,31 +151,8 @@ Here is the task:
task_question=instance['Question'],
)
logger.info(f'Instruction: {instruction}')
image_urls = []
if dest_file:
if extension_name not in ['jpg', 'png', 'zip']:
instruction += f'To solve this task you will have to use the attached file provided in the workspace at location: {dest_file}\n\n'
elif extension_name == 'zip':
filenames = []
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
with zipfile.ZipFile(src_file, 'r') as zip_ref:
filenames = zip_ref.namelist()
filenames = [f'/workspace/{file}' for file in filenames]
filenames = ', '.join(filenames)
instruction += f'To solve this task you will have to use the attached files provided in the workspace at locations: {filenames}\n\n'
else: # Image files: jpg, png
src_file = os.path.join(
DATASET_CACHE_DIR, '2023', metadata.data_split, instance['file_name']
)
instruction += 'Image: To solve this task you will have to use the image shown below.\n\n'
image = Image.open(src_file)
if extension_name == 'jpg':
image_urls.append(image_to_jpg_base64_url(image))
else:
image_urls.append(image_to_png_base64_url(image))
instruction += f'\n\nThe mentioned file is provided in the workspace at: {dest_file.split("/")[-1]}'
instruction += """IMPORTANT: When seeking information from a website, REFRAIN from arbitrary URL navigation. You should utilize the designated search engine tool with precise keywords to obtain relevant URLs or use the specific website's search interface. DO NOT navigate directly to specific URLs as they may not exist.\n\nFor example: if you want to search for a research paper on Arxiv, either use the search engine tool with specific keywords or navigate to arxiv.org and then use its interface.\n"""
instruction += 'IMPORTANT: You should NEVER ask for Human Help.\n'
@@ -221,9 +174,7 @@ Here is the task:
state: State | None = asyncio.run(
run_controller(
config=config,
initial_user_action=MessageAction(
content=instruction, image_urls=image_urls
),
initial_user_action=MessageAction(content=instruction),
runtime=runtime,
fake_user_response_fn=AGENT_CLS_TO_FAKE_USER_RESPONSE_FN[
metadata.agent_class

View File

@@ -1,43 +0,0 @@
import base64
import io
import numpy as np
from PIL import Image
def image_to_png_base64_url(
image: np.ndarray | Image.Image, add_data_prefix: bool = True
):
"""Convert a numpy array to a base64 encoded png image url."""
if isinstance(image, np.ndarray):
image = Image.fromarray(image)
if image.mode in ('RGBA', 'LA'):
image = image.convert('RGB')
buffered = io.BytesIO()
image.save(buffered, format='PNG')
image_base64 = base64.b64encode(buffered.getvalue()).decode()
return (
f'data:image/png;base64,{image_base64}'
if add_data_prefix
else f'{image_base64}'
)
def image_to_jpg_base64_url(
image: np.ndarray | Image.Image, add_data_prefix: bool = True
):
"""Convert a numpy array to a base64 encoded jpeg image url."""
if isinstance(image, np.ndarray):
image = Image.fromarray(image)
if image.mode in ('RGBA', 'LA'):
image = image.convert('RGB')
buffered = io.BytesIO()
image.save(buffered, format='JPEG')
image_base64 = base64.b64encode(buffered.getvalue()).decode()
return (
f'data:image/jpeg;base64,{image_base64}'
if add_data_prefix
else f'{image_base64}'
)

View File

@@ -109,7 +109,7 @@ def codeact_user_response(
) -> str:
encaps_str = (
(
'Your final answer MUST be encapsulated within <solution> and </solution>.\n'
'Please encapsulate your final answer (answer ONLY) within <solution> and </solution>.\n'
'For example: The answer to the question is <solution> 42 </solution>.\n'
)
if encapsulate_solution
@@ -117,7 +117,7 @@ def codeact_user_response(
)
msg = (
'Please continue working on the task on whatever approach you think is suitable.\n'
'When you think you have solved the question, please use the finish tool and include your final answer in the message parameter of the finish tool.\n'
'If you think you have solved the task, please first send your answer to user through message and then finish the interaction.\n'
f'{encaps_str}'
'IMPORTANT: YOU SHOULD NEVER ASK FOR HUMAN HELP.\n'
)

View File

@@ -128,7 +128,7 @@ describe("RepoConnector", () => {
renderRepoConnector();
await screen.findByText("HOME$ADD_GITHUB_REPOS");
await screen.findByText("Add GitHub repos");
});
it("should not render the 'add git(hub|lab) repos' links if oss mode", async () => {

View File

@@ -53,7 +53,7 @@ describe("TaskSuggestions", () => {
it("should render an empty message if there are no tasks", async () => {
getSuggestedTasksSpy.mockResolvedValue([]);
renderTaskSuggestions();
await screen.findByText("TASKS$NO_TASKS_AVAILABLE");
await screen.findByText(/No tasks available/i);
});
it("should render the task groups with the correct titles", async () => {

View File

@@ -473,7 +473,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
await userEvent.clear(nameInput);
await userEvent.type(nameInput, "My_Custom_Secret");
@@ -557,7 +557,7 @@ describe("Secret actions", () => {
// make POST request
expect(createSecretSpy).not.toHaveBeenCalled();
expect(screen.queryByText("SECRETS$SECRET_ALREADY_EXISTS")).toBeInTheDocument();
expect(screen.queryByText(/secret already exists/i)).toBeInTheDocument();
expect(nameInput).toHaveValue(MOCK_GET_SECRETS_RESPONSE[0].name);
expect(valueInput).toHaveValue("my-custom-secret-value");

File diff suppressed because it is too large Load Diff

View File

@@ -1,39 +1,39 @@
{
"name": "openhands-frontend",
"version": "0.47.0",
"version": "0.46.0",
"private": true,
"type": "module",
"engines": {
"node": ">=20.0.0"
},
"dependencies": {
"@heroui/react": "^2.8.0-beta.10",
"@heroui/react": "^2.8.0-beta.9",
"@microlink/react-json-view": "^1.26.2",
"@monaco-editor/react": "^4.7.0-rc.0",
"@react-router/node": "^7.6.3",
"@react-router/serve": "^7.6.3",
"@react-router/node": "^7.6.2",
"@react-router/serve": "^7.6.2",
"@react-types/shared": "^3.29.1",
"@reduxjs/toolkit": "^2.8.2",
"@stripe/react-stripe-js": "^3.7.0",
"@stripe/stripe-js": "^7.4.0",
"@tailwindcss/postcss": "^4.1.11",
"@tailwindcss/vite": "^4.1.11",
"@tanstack/react-query": "^5.81.4",
"@vitejs/plugin-react": "^4.6.0",
"@stripe/stripe-js": "^7.3.1",
"@tailwindcss/postcss": "^4.1.10",
"@tailwindcss/vite": "^4.1.10",
"@tanstack/react-query": "^5.80.10",
"@vitejs/plugin-react": "^4.5.2",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.4.0",
"axios": "^1.10.0",
"clsx": "^2.1.1",
"eslint-config-airbnb-typescript": "^18.0.0",
"framer-motion": "^12.19.2",
"framer-motion": "^12.18.1",
"i18next": "^25.2.1",
"i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^3.0.2",
"isbot": "^5.1.28",
"jose": "^6.0.11",
"lucide-react": "^0.525.0",
"lucide-react": "^0.519.0",
"monaco-editor": "^0.52.2",
"posthog-js": "^1.255.1",
"posthog-js": "^1.255.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-highlight": "^0.15.0",
@@ -42,14 +42,14 @@
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-redux": "^9.2.0",
"react-router": "^7.6.3",
"react-router": "^7.6.2",
"react-syntax-highlighter": "^15.6.1",
"react-textarea-autosize": "^8.5.9",
"remark-gfm": "^4.0.1",
"sirv-cli": "^3.0.1",
"socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1",
"vite": "^7.0.0",
"vite": "^6.3.5",
"web-vitals": "^5.0.3",
"ws": "^8.18.2"
},
@@ -80,19 +80,19 @@
]
},
"devDependencies": {
"@babel/parser": "^7.27.7",
"@babel/traverse": "^7.27.7",
"@babel/parser": "^7.27.1",
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.0",
"@mswjs/socket.io-binding": "^0.2.0",
"@playwright/test": "^1.53.1",
"@react-router/dev": "^7.6.3",
"@react-router/dev": "^7.6.2",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/eslint-plugin-query": "^5.81.2",
"@tanstack/eslint-plugin-query": "^5.78.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.1",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^24.0.5",
"@types/node": "^24.0.3",
"@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6",
"@types/react-highlight": "^0.12.8",
@@ -107,9 +107,9 @@
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-config-prettier": "^10.1.5",
"eslint-plugin-import": "^2.32.0",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.5.1",
"eslint-plugin-prettier": "^5.5.0",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
@@ -117,7 +117,7 @@
"jsdom": "^26.1.0",
"lint-staged": "^16.1.2",
"msw": "^2.6.6",
"prettier": "^3.6.2",
"prettier": "^3.5.3",
"stripe": "^18.2.1",
"tailwindcss": "^4.1.8",
"typescript": "^5.8.3",

View File

@@ -56,6 +56,8 @@ const NON_TEXT_ATTRIBUTES = [
"type",
"href",
"src",
"alt",
"placeholder",
"rel",
"target",
"style",
@@ -63,6 +65,7 @@ const NON_TEXT_ATTRIBUTES = [
"onChange",
"onSubmit",
"data-testid",
"aria-label",
"aria-labelledby",
"aria-describedby",
"aria-hidden",
@@ -136,7 +139,6 @@ function isLikelyCode(str) {
}
function isCommonDevelopmentString(str) {
// Technical patterns that are definitely not UI strings
const technicalPatterns = [
// URLs and paths
@@ -189,7 +191,7 @@ function isCommonDevelopmentString(str) {
// CSS units and values
const cssUnitsPattern =
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
/(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
const cssValuesPattern =
/(rgb|rgba|hsl|hsla|#[0-9a-fA-F]+|solid|absolute|relative|sticky|fixed|static|block|inline|flex|grid|none|auto|hidden|visible)/;
@@ -392,7 +394,6 @@ function isCommonDevelopmentString(str) {
}
function isLikelyUserFacingText(str) {
// Basic validation - skip very short strings or strings without letters
if (!str || str.length <= 2 || !/[a-zA-Z]/.test(str)) {
return false;
@@ -539,8 +540,8 @@ function isInTranslationContext(path) {
}
function scanFileForUnlocalizedStrings(filePath) {
// Skip suggestion content files as they contain special strings that are already properly localized
if (filePath.includes("utils/suggestions/") || filePath.includes("mocks/task-suggestions-handlers.ts")) {
// Skip all suggestion files as they contain special strings
if (filePath.includes("suggestions")) {
return [];
}

View File

@@ -34,7 +34,7 @@ export function ActionSuggestions({
const terms = {
pr,
prShort,
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Check your current branch name first - if it's main, master, deploy, or another common default branch name, create a new branch with a descriptive name related to your changes. Otherwise, use the exact SAME branch name as the one you are currently on.`,
pushToBranch: `Please push the changes to a remote branch on ${getProviderName()}, but do NOT create a ${pr}. Please use the exact SAME branch name as the one you are currently on.`,
createPR: `Please push the changes to ${getProviderName()} and open a ${pr}. Please create a meaningful branch name that describes the changes. If a ${pr} template exists in the repository, please follow it when creating the ${prShort} description.`,
pushToPR: `Please push the latest changes to the existing ${pr}.`,
};

View File

@@ -2,15 +2,15 @@ export function TypingIndicator() {
return (
<div className="flex items-center space-x-1.5 bg-tertiary px-3 py-1.5 rounded-full">
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
style={{ animationDelay: "0ms" }}
/>
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
style={{ animationDelay: "75ms" }}
/>
<span
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[1px]"
className="w-1.5 h-1.5 bg-gray-400 rounded-full animate-[bounce_0.5s_infinite] translate-y-[-2px]"
style={{ animationDelay: "150ms" }}
/>
</div>

View File

@@ -44,7 +44,6 @@ export function LikertScale({
t(I18nKey.FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION),
t(I18nKey.FEEDBACK$REASON_FORGOT_CONTEXT),
t(I18nKey.FEEDBACK$REASON_UNNECESSARY_CHANGES),
t(I18nKey.FEEDBACK$REASON_SHOULD_ASK_FIRST),
t(I18nKey.FEEDBACK$REASON_OTHER),
];

View File

@@ -1,9 +1,6 @@
import { useTranslation } from "react-i18next";
import { useConfig } from "#/hooks/query/use-config";
import { I18nKey } from "#/i18n/declaration";
export function RepoProviderLinks() {
const { t } = useTranslation();
const { data: config } = useConfig();
const githubHref = config
@@ -13,7 +10,7 @@ export function RepoProviderLinks() {
return (
<div className="flex flex-col text-sm underline underline-offset-2 text-content-2 gap-4 w-fit">
<a href={githubHref} target="_blank" rel="noopener noreferrer">
{t(I18nKey.HOME$ADD_GITHUB_REPOS)}
Add GitHub repos
</a>
</div>
);

View File

@@ -1,7 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface BranchDropdownProps {
items: { key: React.Key; label: string }[];
@@ -18,13 +16,11 @@ export function BranchDropdown({
isDisabled,
selectedKey,
}: BranchDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="branch-dropdown"
name="branch-dropdown"
placeholder={t(I18nKey.REPOSITORY$SELECT_BRANCH)}
placeholder="Select a branch"
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@@ -1,7 +1,5 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { SettingsDropdownInput } from "../../settings/settings-dropdown-input";
import { I18nKey } from "#/i18n/declaration";
export interface RepositoryDropdownProps {
items: { key: React.Key; label: string }[];
@@ -16,13 +14,11 @@ export function RepositoryDropdown({
onInputChange,
defaultFilter,
}: RepositoryDropdownProps) {
const { t } = useTranslation();
return (
<SettingsDropdownInput
testId="repo-dropdown"
name="repo-dropdown"
placeholder={t(I18nKey.REPOSITORY$SELECT_REPO)}
placeholder="Select a repo"
items={items}
wrapperClassName="max-w-[500px]"
onSelectionChange={onSelectionChange}

View File

@@ -1,16 +1,13 @@
import { useTranslation } from "react-i18next";
import { TaskGroup } from "./task-group";
import { useSuggestedTasks } from "#/hooks/query/use-suggested-tasks";
import { TaskSuggestionsSkeleton } from "./task-suggestions-skeleton";
import { cn } from "#/utils/utils";
import { I18nKey } from "#/i18n/declaration";
interface TaskSuggestionsProps {
filterFor?: string | null;
}
export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
const { t } = useTranslation();
const { data: tasks, isLoading } = useSuggestedTasks();
const suggestedTasks = filterFor
? tasks?.filter((task) => task.title === filterFor)
@@ -23,13 +20,11 @@ export function TaskSuggestions({ filterFor }: TaskSuggestionsProps) {
data-testid="task-suggestions"
className={cn("flex flex-col w-full", !hasSuggestedTasks && "gap-6")}
>
<h2 className="heading">{t(I18nKey.TASKS$SUGGESTED_TASKS)}</h2>
<h2 className="heading">Suggested Tasks</h2>
<div className="flex flex-col gap-6">
{isLoading && <TaskSuggestionsSkeleton />}
{!hasSuggestedTasks && !isLoading && (
<p>{t(I18nKey.TASKS$NO_TASKS_AVAILABLE)}</p>
)}
{!hasSuggestedTasks && !isLoading && <p>No tasks available</p>}
{suggestedTasks?.map((taskGroup, index) => (
<TaskGroup
key={index}

View File

@@ -64,7 +64,7 @@ export function PaymentForm() {
onChange={handleTopUpInputChange}
type="number"
label={t(I18nKey.PAYMENT$ADD_FUNDS)}
placeholder={t(I18nKey.PAYMENT$SPECIFY_AMOUNT_USD)}
placeholder="Specify an amount in USD to add - min $10"
className="w-[680px]"
min={10}
max={25000}

View File

@@ -1,9 +1,7 @@
import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function BitbucketTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="bitbucket-token-help-anchor" className="text-xs">
<Trans
@@ -11,7 +9,7 @@ export function BitbucketTokenHelpAnchor() {
components={[
<a
key="bitbucket-token-help-anchor-link"
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_HELP_LINK)}
aria-label="Bitbucket token help link"
href="https://bitbucket.org/account/settings/app-passwords/new?scopes=repository:write,pullrequest:write,issue:write"
target="_blank"
className="underline underline-offset-2"
@@ -19,7 +17,7 @@ export function BitbucketTokenHelpAnchor() {
/>,
<a
key="bitbucket-token-help-anchor-link-2"
aria-label={t(I18nKey.GIT$BITBUCKET_TOKEN_SEE_MORE_LINK)}
aria-label="Bitbucket token see more link"
href="https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/"
target="_blank"
className="underline underline-offset-2"

View File

@@ -1,9 +1,7 @@
import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitHubTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="github-token-help-anchor" className="text-xs">
<Trans
@@ -11,7 +9,7 @@ export function GitHubTokenHelpAnchor() {
components={[
<a
key="github-token-help-anchor-link"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_HELP_LINK)}
aria-label="GitHub token help link"
href="https://github.com/settings/tokens/new?description=openhands-app&scopes=repo,user,workflow"
target="_blank"
className="underline underline-offset-2"
@@ -19,7 +17,7 @@ export function GitHubTokenHelpAnchor() {
/>,
<a
key="github-token-help-anchor-link-2"
aria-label={t(I18nKey.GIT$GITHUB_TOKEN_SEE_MORE_LINK)}
aria-label="GitHub token see more link"
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token"
target="_blank"
className="underline underline-offset-2"

View File

@@ -1,9 +1,7 @@
import { Trans, useTranslation } from "react-i18next";
import { Trans } from "react-i18next";
import { I18nKey } from "#/i18n/declaration";
export function GitLabTokenHelpAnchor() {
const { t } = useTranslation();
return (
<p data-testid="gitlab-token-help-anchor" className="text-xs">
<Trans
@@ -11,7 +9,7 @@ export function GitLabTokenHelpAnchor() {
components={[
<a
key="gitlab-token-help-anchor-link"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_HELP_LINK)}
aria-label="Gitlab token help link"
href="https://gitlab.com/-/user_settings/personal_access_tokens?name=openhands-app&scopes=api,read_user,read_repository,write_repository"
target="_blank"
className="underline underline-offset-2"
@@ -19,7 +17,7 @@ export function GitLabTokenHelpAnchor() {
/>,
<a
key="gitlab-token-help-anchor-link-2"
aria-label={t(I18nKey.GIT$GITLAB_TOKEN_SEE_MORE_LINK)}
aria-label="GitLab token see more link"
href="https://docs.gitlab.com/user/profile/personal_access_tokens/"
target="_blank"
className="underline underline-offset-2"

View File

@@ -111,7 +111,7 @@ export function SecretForm({
(secret) => secret.name === name && secret.name !== selectedSecret,
);
if (isNameAlreadyUsed) {
setError(t("SECRETS$SECRET_ALREADY_EXISTS"));
setError("Secret already exists");
return;
}
@@ -144,7 +144,7 @@ export function SecretForm({
className="w-full max-w-[350px]"
required
defaultValue={mode === "edit" && selectedSecret ? selectedSecret : ""}
placeholder={t("SECRETS$API_KEY_EXAMPLE")}
placeholder="e.g. OpenAI_API_Key"
pattern="^\S*$"
/>
{error && <p className="text-red-500 text-sm">{error}</p>}

View File

@@ -7,7 +7,6 @@ import { ModalBody } from "#/components/shared/modals/modal-body";
import { BrandButton } from "../settings/brand-button";
import GitHubLogo from "#/assets/branding/github-logo.svg?react";
import GitLabLogo from "#/assets/branding/gitlab-logo.svg?react";
import BitbucketLogo from "#/assets/branding/bitbucket-logo.svg?react";
import { useAuthUrl } from "#/hooks/use-auth-url";
import { GetConfigResponse } from "#/api/open-hands.types";
@@ -24,11 +23,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
identityProvider: "gitlab",
});
const bitbucketAuthUrl = useAuthUrl({
appMode: appMode || null,
identityProvider: "bitbucket",
});
const handleGitHubAuth = () => {
if (githubAuthUrl) {
// Always start the OIDC flow, let the backend handle TOS check
@@ -43,13 +37,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
}
};
const handleBitbucketAuth = () => {
if (bitbucketAuthUrl) {
// Always start the OIDC flow, let the backend handle TOS check
window.location.href = bitbucketAuthUrl;
}
};
return (
<ModalBackdrop>
<ModalBody className="border border-tertiary">
@@ -80,16 +67,6 @@ export function AuthModal({ githubAuthUrl, appMode }: AuthModalProps) {
>
{t(I18nKey.GITLAB$CONNECT_TO_GITLAB)}
</BrandButton>
<BrandButton
type="button"
variant="primary"
onClick={handleBitbucketAuth}
className="w-full"
startContent={<BitbucketLogo width={20} height={20} />}
>
{t(I18nKey.BITBUCKET$CONNECT_TO_BITBUCKET)}
</BrandButton>
</div>
</ModalBody>
</ModalBackdrop>

View File

@@ -28,7 +28,7 @@ export function CustomModelInput({
id="custom-model"
name="custom-model"
defaultValue={defaultValue}
aria-label={t(I18nKey.MODEL$CUSTOM_MODEL)}
aria-label="Custom Model"
classNames={{
inputWrapper: "bg-[#27272A] rounded-md text-sm px-3 py-[10px]",
}}

View File

@@ -198,46 +198,46 @@ function SecurityInvariant() {
{t(I18nKey.INVARIANT$ASK_CONFIRMATION_RISK_SEVERITY_LABEL)}
</p>
<Select
placeholder={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
placeholder="Select risk severity"
value={selectedRisk}
onChange={(e) =>
setSelectedRisk(Number(e.target.value) as ActionSecurityRisk)
}
className={getRiskColor(selectedRisk)}
selectedKeys={new Set([selectedRisk.toString()])}
aria-label={t(I18nKey.SECURITY$SELECT_RISK_SEVERITY)}
aria-label="Select risk severity"
>
<SelectItem
key={ActionSecurityRisk.UNKNOWN}
aria-label={t(I18nKey.SECURITY$UNKNOWN_RISK)}
aria-label="Unknown Risk"
className={getRiskColor(ActionSecurityRisk.UNKNOWN)}
>
{getRiskText(ActionSecurityRisk.UNKNOWN)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.LOW}
aria-label={t(I18nKey.SECURITY$LOW_RISK)}
aria-label="Low Risk"
className={getRiskColor(ActionSecurityRisk.LOW)}
>
{getRiskText(ActionSecurityRisk.LOW)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.MEDIUM}
aria-label={t(I18nKey.SECURITY$MEDIUM_RISK)}
aria-label="Medium Risk"
className={getRiskColor(ActionSecurityRisk.MEDIUM)}
>
{getRiskText(ActionSecurityRisk.MEDIUM)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH}
aria-label={t(I18nKey.SECURITY$HIGH_RISK)}
aria-label="High Risk"
className={getRiskColor(ActionSecurityRisk.HIGH)}
>
{getRiskText(ActionSecurityRisk.HIGH)}
</SelectItem>
<SelectItem
key={ActionSecurityRisk.HIGH + 1}
aria-label={t(I18nKey.SECURITY$DONT_ASK_CONFIRMATION)}
aria-label="Don't ask for confirmation"
>
{t(I18nKey.INVARIANT$DONT_ASK_FOR_CONFIRMATION_LABEL)}
</SelectItem>

View File

@@ -34,7 +34,10 @@ export const useAuthCallback = () => {
const loginMethod = searchParams.get("login_method");
// Set the login method if it's valid
if (Object.values(LoginMethod).includes(loginMethod as LoginMethod)) {
if (
loginMethod === LoginMethod.GITHUB ||
loginMethod === LoginMethod.GITLAB
) {
setLoginMethod(loginMethod as LoginMethod);
// Clean up the URL by removing the login_method parameter

View File

@@ -602,30 +602,7 @@ export enum I18nKey {
FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION = "FEEDBACK$REASON_MISUNDERSTOOD_INSTRUCTION",
FEEDBACK$REASON_FORGOT_CONTEXT = "FEEDBACK$REASON_FORGOT_CONTEXT",
FEEDBACK$REASON_UNNECESSARY_CHANGES = "FEEDBACK$REASON_UNNECESSARY_CHANGES",
FEEDBACK$REASON_SHOULD_ASK_FIRST = "FEEDBACK$REASON_SHOULD_ASK_FIRST",
FEEDBACK$REASON_OTHER = "FEEDBACK$REASON_OTHER",
FEEDBACK$THANK_YOU_FOR_FEEDBACK = "FEEDBACK$THANK_YOU_FOR_FEEDBACK",
FEEDBACK$FAILED_TO_SUBMIT = "FEEDBACK$FAILED_TO_SUBMIT",
HOME$ADD_GITHUB_REPOS = "HOME$ADD_GITHUB_REPOS",
REPOSITORY$SELECT_BRANCH = "REPOSITORY$SELECT_BRANCH",
REPOSITORY$SELECT_REPO = "REPOSITORY$SELECT_REPO",
TASKS$SUGGESTED_TASKS = "TASKS$SUGGESTED_TASKS",
TASKS$NO_TASKS_AVAILABLE = "TASKS$NO_TASKS_AVAILABLE",
PAYMENT$SPECIFY_AMOUNT_USD = "PAYMENT$SPECIFY_AMOUNT_USD",
GIT$BITBUCKET_TOKEN_HELP_LINK = "GIT$BITBUCKET_TOKEN_HELP_LINK",
GIT$BITBUCKET_TOKEN_SEE_MORE_LINK = "GIT$BITBUCKET_TOKEN_SEE_MORE_LINK",
GIT$GITHUB_TOKEN_HELP_LINK = "GIT$GITHUB_TOKEN_HELP_LINK",
GIT$GITHUB_TOKEN_SEE_MORE_LINK = "GIT$GITHUB_TOKEN_SEE_MORE_LINK",
GIT$GITLAB_TOKEN_HELP_LINK = "GIT$GITLAB_TOKEN_HELP_LINK",
GIT$GITLAB_TOKEN_SEE_MORE_LINK = "GIT$GITLAB_TOKEN_SEE_MORE_LINK",
SECRETS$SECRET_ALREADY_EXISTS = "SECRETS$SECRET_ALREADY_EXISTS",
SECRETS$API_KEY_EXAMPLE = "SECRETS$API_KEY_EXAMPLE",
MODEL$CUSTOM_MODEL = "MODEL$CUSTOM_MODEL",
SECURITY$SELECT_RISK_SEVERITY = "SECURITY$SELECT_RISK_SEVERITY",
SECURITY$DONT_ASK_CONFIRMATION = "SECURITY$DONT_ASK_CONFIRMATION",
SETTINGS$MAXIMUM_BUDGET_USD = "SETTINGS$MAXIMUM_BUDGET_USD",
GIT$DISCONNECT_TOKENS = "GIT$DISCONNECT_TOKENS",
API$TAVILY_KEY_EXAMPLE = "API$TAVILY_KEY_EXAMPLE",
API$TVLY_KEY_EXAMPLE = "API$TVLY_KEY_EXAMPLE",
SECRETS$CONNECT_GIT_PROVIDER = "SECRETS$CONNECT_GIT_PROVIDER",
}

View File

@@ -9631,22 +9631,6 @@
"de": "Der Agent hat unnötige Änderungen vorgenommen",
"uk": "Агент зробив непотрібні зміни"
},
"FEEDBACK$REASON_SHOULD_ASK_FIRST": {
"en": "The agent should've asked me first before doing it!",
"ja": "エージェントは実行する前に私に確認すべきでした!",
"zh-CN": "代理应该先问我再做这件事!",
"zh-TW": "代理應該先問我再做這件事!",
"ko-KR": "에이전트는 먼저 나에게 물어봤어야 했습니다!",
"no": "Agenten burde ha spurt meg først før den gjorde det!",
"it": "L'agente avrebbe dovuto chiedermelo prima di farlo!",
"pt": "O agente deveria ter me perguntado primeiro antes de fazer isso!",
"es": "¡El agente debería haberme preguntado primero antes de hacerlo!",
"ar": "كان يجب على الوكيل أن يسألني أولاً قبل القيام بذلك!",
"fr": "L'agent aurait dû me demander d'abord avant de le faire !",
"tr": "Ajan bunu yapmadan önce bana sormalıydı!",
"de": "Der Agent hätte mich vorher fragen sollen!",
"uk": "Агент повинен був спочатку запитати мене, перш ніж це робити!"
},
"FEEDBACK$REASON_OTHER": {
"en": "Other",
"ja": "その他",
@@ -9694,357 +9678,5 @@
"tr": "Geri bildirim gönderilemedi",
"de": "Feedback konnte nicht gesendet werden",
"uk": "Не вдалося надіслати відгук"
},
"HOME$ADD_GITHUB_REPOS": {
"en": "Add GitHub repos",
"ja": "GitHubリポジトリを追加",
"zh-CN": "添加GitHub仓库",
"zh-TW": "新增GitHub儲存庫",
"ko-KR": "GitHub 저장소 추가",
"no": "Legg til GitHub-repositorier",
"it": "Aggiungi repository GitHub",
"pt": "Adicionar repositórios GitHub",
"es": "Agregar repositorios de GitHub",
"ar": "إضافة مستودعات GitHub",
"fr": "Ajouter des dépôts GitHub",
"tr": "GitHub depoları ekle",
"de": "GitHub-Repositories hinzufügen",
"uk": "Додати репозиторії GitHub"
},
"REPOSITORY$SELECT_BRANCH": {
"en": "Select a branch",
"ja": "ブランチを選択",
"zh-CN": "选择分支",
"zh-TW": "選擇分支",
"ko-KR": "브랜치 선택",
"no": "Velg en gren",
"it": "Seleziona un ramo",
"pt": "Selecionar um branch",
"es": "Seleccionar una rama",
"ar": "اختر فرع",
"fr": "Sélectionner une branche",
"tr": "Bir dal seç",
"de": "Einen Branch auswählen",
"uk": "Вибрати гілку"
},
"REPOSITORY$SELECT_REPO": {
"en": "Select a repo",
"ja": "リポジトリを選択",
"zh-CN": "选择仓库",
"zh-TW": "選擇儲存庫",
"ko-KR": "저장소 선택",
"no": "Velg et repositorium",
"it": "Seleziona un repository",
"pt": "Selecionar um repositório",
"es": "Seleccionar un repositorio",
"ar": "اختر مستودع",
"fr": "Sélectionner un dépôt",
"tr": "Bir depo seç",
"de": "Ein Repository auswählen",
"uk": "Вибрати репозиторій"
},
"TASKS$SUGGESTED_TASKS": {
"en": "Suggested Tasks",
"ja": "推奨タスク",
"zh-CN": "建议任务",
"zh-TW": "建議任務",
"ko-KR": "추천 작업",
"no": "Foreslåtte oppgaver",
"it": "Attività suggerite",
"pt": "Tarefas sugeridas",
"es": "Tareas sugeridas",
"ar": "المهام المقترحة",
"fr": "Tâches suggérées",
"tr": "Önerilen görevler",
"de": "Vorgeschlagene Aufgaben",
"uk": "Запропоновані завдання"
},
"TASKS$NO_TASKS_AVAILABLE": {
"en": "No tasks available",
"ja": "利用可能なタスクがありません",
"zh-CN": "没有可用任务",
"zh-TW": "沒有可用任務",
"ko-KR": "사용 가능한 작업이 없습니다",
"no": "Ingen oppgaver tilgjengelig",
"it": "Nessuna attività disponibile",
"pt": "Nenhuma tarefa disponível",
"es": "No hay tareas disponibles",
"ar": "لا توجد مهام متاحة",
"fr": "Aucune tâche disponible",
"tr": "Mevcut görev yok",
"de": "Keine Aufgaben verfügbar",
"uk": "Немає доступних завдань"
},
"PAYMENT$SPECIFY_AMOUNT_USD": {
"en": "Specify an amount in USD to add - min $10",
"ja": "追加するUSD金額を指定してください - 最小$10",
"zh-CN": "指定要添加的美元金额 - 最少$10",
"zh-TW": "指定要新增的美元金額 - 最少$10",
"ko-KR": "추가할 USD 금액을 지정하세요 - 최소 $10",
"no": "Spesifiser et beløp i USD å legge til - min $10",
"it": "Specifica un importo in USD da aggiungere - min $10",
"pt": "Especifique um valor em USD para adicionar - mín $10",
"es": "Especifique una cantidad en USD para agregar - mín $10",
"ar": "حدد مبلغًا بالدولار الأمريكي لإضافته - الحد الأدنى 10 دولارات",
"fr": "Spécifiez un montant en USD à ajouter - min 10 $",
"tr": "Eklenecek USD tutarını belirtin - min $10",
"de": "Geben Sie einen USD-Betrag zum Hinzufügen an - min $10",
"uk": "Вкажіть суму в доларах США для додавання - мін $10"
},
"GIT$BITBUCKET_TOKEN_HELP_LINK": {
"en": "Bitbucket token help link",
"ja": "Bitbucketトークンヘルプリンク",
"zh-CN": "Bitbucket令牌帮助链接",
"zh-TW": "Bitbucket令牌幫助連結",
"ko-KR": "Bitbucket 토큰 도움말 링크",
"no": "Bitbucket token hjelpelenke",
"it": "Link di aiuto per il token Bitbucket",
"pt": "Link de ajuda do token Bitbucket",
"es": "Enlace de ayuda del token de Bitbucket",
"ar": "رابط مساعدة رمز Bitbucket",
"fr": "Lien d'aide pour le jeton Bitbucket",
"tr": "Bitbucket token yardım bağlantısı",
"de": "Bitbucket-Token-Hilfe-Link",
"uk": "Посилання на довідку токена Bitbucket"
},
"GIT$BITBUCKET_TOKEN_SEE_MORE_LINK": {
"en": "Bitbucket token see more link",
"ja": "Bitbucketトークン詳細リンク",
"zh-CN": "Bitbucket令牌查看更多链接",
"zh-TW": "Bitbucket令牌查看更多連結",
"ko-KR": "Bitbucket 토큰 더 보기 링크",
"no": "Bitbucket token se mer lenke",
"it": "Link per vedere di più sul token Bitbucket",
"pt": "Link para ver mais sobre o token Bitbucket",
"es": "Enlace para ver más del token de Bitbucket",
"ar": "رابط لرؤية المزيد حول رمز Bitbucket",
"fr": "Lien pour en voir plus sur le jeton Bitbucket",
"tr": "Bitbucket token daha fazla görme bağlantısı",
"de": "Bitbucket-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен Bitbucket"
},
"GIT$GITHUB_TOKEN_HELP_LINK": {
"en": "GitHub token help link",
"ja": "GitHubトークンヘルプリンク",
"zh-CN": "GitHub令牌帮助链接",
"zh-TW": "GitHub令牌幫助連結",
"ko-KR": "GitHub 토큰 도움말 링크",
"no": "GitHub token hjelpelenke",
"it": "Link di aiuto per il token GitHub",
"pt": "Link de ajuda do token GitHub",
"es": "Enlace de ayuda del token de GitHub",
"ar": "رابط مساعدة رمز GitHub",
"fr": "Lien d'aide pour le jeton GitHub",
"tr": "GitHub token yardım bağlantısı",
"de": "GitHub-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitHub"
},
"GIT$GITHUB_TOKEN_SEE_MORE_LINK": {
"en": "GitHub token see more link",
"ja": "GitHubトークン詳細リンク",
"zh-CN": "GitHub令牌查看更多链接",
"zh-TW": "GitHub令牌查看更多連結",
"ko-KR": "GitHub 토큰 더 보기 링크",
"no": "GitHub token se mer lenke",
"it": "Link per vedere di più sul token GitHub",
"pt": "Link para ver mais sobre o token GitHub",
"es": "Enlace para ver más del token de GitHub",
"ar": "رابط لرؤية المزيد حول رمز GitHub",
"fr": "Lien pour en voir plus sur le jeton GitHub",
"tr": "GitHub token daha fazla görme bağlantısı",
"de": "GitHub-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitHub"
},
"GIT$GITLAB_TOKEN_HELP_LINK": {
"en": "Gitlab token help link",
"ja": "GitLabトークンヘルプリンク",
"zh-CN": "GitLab令牌帮助链接",
"zh-TW": "GitLab令牌幫助連結",
"ko-KR": "GitLab 토큰 도움말 링크",
"no": "GitLab token hjelpelenke",
"it": "Link di aiuto per il token GitLab",
"pt": "Link de ajuda do token GitLab",
"es": "Enlace de ayuda del token de GitLab",
"ar": "رابط مساعدة رمز GitLab",
"fr": "Lien d'aide pour le jeton GitLab",
"tr": "GitLab token yardım bağlantısı",
"de": "GitLab-Token-Hilfe-Link",
"uk": "Посилання на довідку токена GitLab"
},
"GIT$GITLAB_TOKEN_SEE_MORE_LINK": {
"en": "GitLab token see more link",
"ja": "GitLabトークン詳細リンク",
"zh-CN": "GitLab令牌查看更多链接",
"zh-TW": "GitLab令牌查看更多連結",
"ko-KR": "GitLab 토큰 더 보기 링크",
"no": "GitLab token se mer lenke",
"it": "Link per vedere di più sul token GitLab",
"pt": "Link para ver mais sobre o token GitLab",
"es": "Enlace para ver más del token de GitLab",
"ar": "رابط لرؤية المزيد حول رمز GitLab",
"fr": "Lien pour en voir plus sur le jeton GitLab",
"tr": "GitLab token daha fazla görme bağlantısı",
"de": "GitLab-Token mehr sehen Link",
"uk": "Посилання для перегляду більше про токен GitLab"
},
"SECRETS$SECRET_ALREADY_EXISTS": {
"en": "Secret already exists",
"ja": "シークレットは既に存在します",
"zh-CN": "密钥已存在",
"zh-TW": "密鑰已存在",
"ko-KR": "시크릿이 이미 존재합니다",
"no": "Hemmelighet eksisterer allerede",
"it": "Il segreto esiste già",
"pt": "Segredo já existe",
"es": "El secreto ya existe",
"ar": "السر موجود بالفعل",
"fr": "Le secret existe déjà",
"tr": "Gizli anahtar zaten mevcut",
"de": "Geheimnis existiert bereits",
"uk": "Секрет вже існує"
},
"SECRETS$API_KEY_EXAMPLE": {
"en": "e.g. OpenAI_API_Key",
"ja": "例: OpenAI_API_Key",
"zh-CN": "例如 OpenAI_API_Key",
"zh-TW": "例如 OpenAI_API_Key",
"ko-KR": "예: OpenAI_API_Key",
"no": "f.eks. OpenAI_API_Key",
"it": "es. OpenAI_API_Key",
"pt": "ex. OpenAI_API_Key",
"es": "ej. OpenAI_API_Key",
"ar": "مثل OpenAI_API_Key",
"fr": "ex. OpenAI_API_Key",
"tr": "örn. OpenAI_API_Key",
"de": "z.B. OpenAI_API_Key",
"uk": "наприклад OpenAI_API_Key"
},
"MODEL$CUSTOM_MODEL": {
"en": "Custom Model",
"ja": "カスタムモデル",
"zh-CN": "自定义模型",
"zh-TW": "自訂模型",
"ko-KR": "사용자 정의 모델",
"no": "Tilpasset modell",
"it": "Modello personalizzato",
"pt": "Modelo personalizado",
"es": "Modelo personalizado",
"ar": "نموذج مخصص",
"fr": "Modèle personnalisé",
"tr": "Özel model",
"de": "Benutzerdefiniertes Modell",
"uk": "Користувацька модель"
},
"SECURITY$SELECT_RISK_SEVERITY": {
"en": "Select risk severity",
"ja": "リスクの重要度を選択",
"zh-CN": "选择风险严重程度",
"zh-TW": "選擇風險嚴重程度",
"ko-KR": "위험 심각도 선택",
"no": "Velg risikoalvorlighet",
"it": "Seleziona gravità del rischio",
"pt": "Selecionar gravidade do risco",
"es": "Seleccionar gravedad del riesgo",
"ar": "اختر شدة المخاطر",
"fr": "Sélectionner la gravité du risque",
"tr": "Risk ciddiyetini seç",
"de": "Risikoschweregrad auswählen",
"uk": "Вибрати ступінь ризику"
},
"SECURITY$DONT_ASK_CONFIRMATION": {
"en": "Don't ask for confirmation",
"ja": "確認を求めない",
"zh-CN": "不要求确认",
"zh-TW": "不要求確認",
"ko-KR": "확인을 요청하지 않음",
"no": "Ikke spør om bekreftelse",
"it": "Non chiedere conferma",
"pt": "Não pedir confirmação",
"es": "No pedir confirmación",
"ar": "لا تطلب التأكيد",
"fr": "Ne pas demander de confirmation",
"tr": "Onay isteme",
"de": "Nicht nach Bestätigung fragen",
"uk": "Не запитувати підтвердження"
},
"SETTINGS$MAXIMUM_BUDGET_USD": {
"en": "Maximum budget per conversation in USD",
"ja": "会話あたりの最大予算USD",
"zh-CN": "每次对话的最大预算(美元)",
"zh-TW": "每次對話的最大預算(美元)",
"ko-KR": "대화당 최대 예산(USD)",
"no": "Maksimalt budsjett per samtale i USD",
"it": "Budget massimo per conversazione in USD",
"pt": "Orçamento máximo por conversa em USD",
"es": "Presupuesto máximo por conversación en USD",
"ar": "الحد الأقصى للميزانية لكل محادثة بالدولار الأمريكي",
"fr": "Budget maximum par conversation en USD",
"tr": "Konuşma başına maksimum bütçe (USD)",
"de": "Maximales Budget pro Gespräch in USD",
"uk": "Максимальний бюджет на розмову в доларах США"
},
"GIT$DISCONNECT_TOKENS": {
"en": "Disconnect Tokens",
"ja": "トークンを切断",
"zh-CN": "断开令牌连接",
"zh-TW": "中斷令牌連接",
"ko-KR": "토큰 연결 해제",
"no": "Koble fra tokens",
"it": "Disconnetti token",
"pt": "Desconectar tokens",
"es": "Desconectar tokens",
"ar": "قطع اتصال الرموز",
"fr": "Déconnecter les jetons",
"tr": "Token bağlantısını kes",
"de": "Token trennen",
"uk": "Відключити токени"
},
"API$TAVILY_KEY_EXAMPLE": {
"en": "sk-tavily-...",
"ja": "sk-tavily-...",
"zh-CN": "sk-tavily-...",
"zh-TW": "sk-tavily-...",
"ko-KR": "sk-tavily-...",
"no": "sk-tavily-...",
"it": "sk-tavily-...",
"pt": "sk-tavily-...",
"es": "sk-tavily-...",
"ar": "sk-tavily-...",
"fr": "sk-tavily-...",
"tr": "sk-tavily-...",
"de": "sk-tavily-...",
"uk": "sk-tavily-..."
},
"API$TVLY_KEY_EXAMPLE": {
"en": "tvly-...",
"ja": "tvly-...",
"zh-CN": "tvly-...",
"zh-TW": "tvly-...",
"ko-KR": "tvly-...",
"no": "tvly-...",
"it": "tvly-...",
"pt": "tvly-...",
"es": "tvly-...",
"ar": "tvly-...",
"fr": "tvly-...",
"tr": "tvly-...",
"de": "tvly-...",
"uk": "tvly-..."
},
"SECRETS$CONNECT_GIT_PROVIDER": {
"en": "Connect a Git provider to manage secrets",
"ja": "シークレットを管理するためにGitプロバイダーに接続",
"zh-CN": "连接Git提供商以管理密钥",
"zh-TW": "連接Git提供商以管理密鑰",
"ko-KR": "시크릿 관리를 위해 Git 제공자에 연결",
"no": "Koble til en Git-leverandør for å administrere hemmeligheter",
"it": "Connetti un provider Git per gestire i segreti",
"pt": "Conectar um provedor Git para gerenciar segredos",
"es": "Conectar un proveedor Git para gestionar secretos",
"ar": "اتصل بمزود Git لإدارة الأسرار",
"fr": "Connecter un fournisseur Git pour gérer les secrets",
"tr": "Gizli anahtarları yönetmek için bir Git sağlayıcısına bağlan",
"de": "Git-Anbieter verbinden, um Geheimnisse zu verwalten",
"uk": "Підключити провайдера Git для управління секретами"
}
}

View File

@@ -189,7 +189,7 @@ function AppSettingsScreen() {
label={t(I18nKey.SETTINGS$MAX_BUDGET_PER_CONVERSATION)}
defaultValue={settings.MAX_BUDGET_PER_TASK?.toString() || ""}
onChange={checkIfMaxBudgetPerTaskHasChanged}
placeholder={t(I18nKey.SETTINGS$MAXIMUM_BUDGET_USD)}
placeholder="Maximum budget per conversation in USD"
min={1}
step={1}
className="w-[680px]" // Match the width of the language field

View File

@@ -185,7 +185,7 @@ function GitSettingsScreen() {
!isGitHubTokenSet && !isGitLabTokenSet && !isBitbucketTokenSet
}
>
{t(I18nKey.GIT$DISCONNECT_TOKENS)}
Disconnect Tokens
</BrandButton>
<BrandButton
testId="submit-button"

View File

@@ -318,7 +318,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder={t(I18nKey.API$TAVILY_KEY_EXAMPLE)}
placeholder="sk-tavily-..."
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />
@@ -393,7 +393,7 @@ function LlmSettingsScreen() {
className="w-full max-w-[680px]"
defaultValue={settings.SEARCH_API_KEY || ""}
onChange={handleSearchApiKeyIsDirty}
placeholder={t(I18nKey.API$TVLY_KEY_EXAMPLE)}
placeholder="tvly-..."
startContent={
settings.SEARCH_API_KEY_SET && (
<KeyStatusIcon isSet={settings.SEARCH_API_KEY_SET} />

View File

@@ -13,7 +13,6 @@ import { BrandButton } from "#/components/features/settings/brand-button";
import { ConfirmationModal } from "#/components/shared/modals/confirmation-modal";
import { GetSecretsResponse } from "#/api/secrets-service.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { I18nKey } from "#/i18n/declaration";
import { useConfig } from "#/hooks/query/use-config";
function SecretsSettingsScreen() {
@@ -91,7 +90,7 @@ function SecretsSettingsScreen() {
type="button"
>
<BrandButton type="button" variant="secondary">
{t(I18nKey.SECRETS$CONNECT_GIT_PROVIDER)}
Connect a Git provider to manage secrets
</BrandButton>
</Link>
)}

View File

@@ -13,10 +13,8 @@ export function handleObservationMessage(message: ObservationMessage) {
let { content } = message;
if (content.length > 5000) {
const halfLength = 2500;
const head = content.slice(0, halfLength);
const tail = content.slice(content.length - halfLength);
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...\r\n\n${tail}`;
const head = content.slice(0, 5000);
content = `${head}\r\n\n... (truncated ${message.content.length - 5000} characters) ...`;
}
store.dispatch(appendOutput(content));

View File

@@ -1,81 +0,0 @@
import { describe, it, expect } from "vitest";
import { execSync } from "child_process";
import path from "path";
import fs from "fs";
describe("Localization Fix Tests", () => {
it("should not find any unlocalized strings in the frontend code", () => {
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
// Run the localization check script
const result = execSync(`node ${scriptPath}`, {
cwd: path.join(__dirname, "../.."),
encoding: "utf8",
});
// The script should output success message and exit with code 0
expect(result).toContain(
"✅ No unlocalized strings found in frontend code.",
);
});
it("should properly detect user-facing attributes like placeholder, alt, and aria-label", () => {
// This test verifies that our fix to include placeholder, alt, and aria-label
// attributes in the localization check is working correctly by testing the regex patterns
const scriptPath = path.join(
__dirname,
"../../scripts/check-unlocalized-strings.cjs",
);
const scriptContent = fs.readFileSync(scriptPath, "utf8");
// Verify that these attributes are now being checked for localization
// by ensuring they're not excluded from text extraction
const nonTextAttributesMatch = scriptContent.match(
/const NON_TEXT_ATTRIBUTES = \[(.*?)\]/s,
);
expect(nonTextAttributesMatch).toBeTruthy();
const nonTextAttributes = nonTextAttributesMatch![1];
expect(nonTextAttributes).not.toContain('"placeholder"');
expect(nonTextAttributes).not.toContain('"alt"');
expect(nonTextAttributes).not.toContain('"aria-label"');
// Verify that the script contains the correct attributes that should be excluded
expect(nonTextAttributes).toContain('"className"');
expect(nonTextAttributes).toContain('"testId"');
expect(nonTextAttributes).toContain('"href"');
});
it("should not incorrectly flag CSS units as unlocalized strings", () => {
// This test verifies that our fix to the CSS units regex pattern
// prevents false positives like "Suggested Tasks" being flagged
const testStrings = [
"Suggested Tasks",
"No tasks available",
"Select a branch",
"Select a repo",
"Custom Models",
"API Keys",
"Git Settings",
];
// These strings should not be flagged as CSS units
const cssUnitsPattern =
/\b\d+(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$|^(px|rem|em|vh|vw|vmin|vmax|ch|ex|fr|deg|rad|turn|grad|ms|s)$/;
testStrings.forEach((str) => {
expect(cssUnitsPattern.test(str)).toBe(false);
});
// But actual CSS units should still be detected
const actualCssUnits = ["10px", "2rem", "100vh", "px", "rem", "s"];
actualCssUnits.forEach((unit) => {
expect(cssUnitsPattern.test(unit)).toBe(true);
});
});
});

View File

@@ -12,9 +12,6 @@ if TYPE_CHECKING:
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
from openhands.agenthub.codeact_agent.tools.bash import create_cmd_run_tool
from openhands.agenthub.codeact_agent.tools.browser import BrowserTool
from openhands.agenthub.codeact_agent.tools.condensation_request import (
CondensationRequestTool,
)
from openhands.agenthub.codeact_agent.tools.finish import FinishTool
from openhands.agenthub.codeact_agent.tools.ipython import IPythonTool
from openhands.agenthub.codeact_agent.tools.llm_based_edit import LLMBasedFileEditTool
@@ -122,8 +119,6 @@ class CodeActAgent(Agent):
tools.append(ThinkTool)
if self.config.enable_finish:
tools.append(FinishTool)
if self.config.enable_condensation_request:
tools.append(CondensationRequestTool)
if self.config.enable_browsing:
if sys.platform == 'win32':
logger.warning('Windows runtime does not support browsing yet')

View File

@@ -11,7 +11,6 @@ from litellm import (
from openhands.agenthub.codeact_agent.tools import (
BrowserTool,
CondensationRequestTool,
FinishTool,
IPythonTool,
LLMBasedFileEditTool,
@@ -36,7 +35,6 @@ from openhands.events.action import (
IPythonRunCellAction,
MessageAction,
)
from openhands.events.action.agent import CondensationRequestAction
from openhands.events.action.mcp import MCPAction
from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.tool import ToolCallMetadata
@@ -205,12 +203,6 @@ def response_to_actions(
elif tool_call.function.name == ThinkTool['function']['name']:
action = AgentThinkAction(thought=arguments.get('thought', ''))
# ================================================
# CondensationRequestAction
# ================================================
elif tool_call.function.name == CondensationRequestTool['function']['name']:
action = CondensationRequestAction()
# ================================================
# BrowserTool
# ================================================

View File

@@ -32,7 +32,6 @@ Your primary role is to assist users by executing commands, modifying code, and
</VERSION_CONTROL>
<PULL_REQUESTS>
* **Important**: Do not push to the remote branch and/or start a pull request unless explicitly asked to do so.
* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise.
* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue.
* When updating a PR, preserve the original PR title and purpose, updating description only when necessary.

View File

@@ -1,111 +0,0 @@
You are OpenHands agent, a helpful AI assistant that can interact with a computer to solve tasks.
<ROLE>
Your primary role is to assist users by executing commands, modifying code, and solving technical problems effectively. You should be thorough, methodical, and prioritize quality over speed.
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
</ROLE>
<EFFICIENCY>
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
</EFFICIENCY>
<FILE_SYSTEM_GUIDELINES>
* When a user provides a file path, do NOT assume it's relative to the current working directory. First explore the file system to locate the file before working on it.
* If asked to edit a file, edit the file directly, rather than creating a new file with a different filename.
* For global search-and-replace operations, consider using `sed` instead of opening file editors multiple times.
</FILE_SYSTEM_GUIDELINES>
<CODE_QUALITY>
* Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
* When implementing solutions, focus on making the minimal changes needed to solve the problem.
* Before implementing any changes, first thoroughly understand the codebase through exploration.
* If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.
</CODE_QUALITY>
<VERSION_CONTROL>
* When configuring git credentials, use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
</VERSION_CONTROL>
<PULL_REQUESTS>
* When creating pull requests, create only ONE per session/issue unless explicitly instructed otherwise.
* When working with an existing PR, update it with new commits rather than creating additional PRs for the same issue.
* When updating a PR, preserve the original PR title and purpose, updating description only when necessary.
</PULL_REQUESTS>
<PROBLEM_SOLVING_WORKFLOW>
1. EXPLORATION: Thoroughly explore relevant files and understand the context before proposing solutions
2. ANALYSIS: Consider multiple approaches and select the most promising one
3. TESTING:
* For bug fixes: Create tests to verify issues before implementing fixes
* For new features: Consider test-driven development when appropriate
* If the repository lacks testing infrastructure and implementing tests would require extensive setup, consult with the user before investing time in building testing infrastructure
* If the environment is not set up to run tests, consult with the user first before investing time to install all dependencies
4. IMPLEMENTATION: Make focused, minimal changes to address the problem
5. VERIFICATION: 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>
<TASK_MANAGEMENT>
* For complex, long-horizon tasks, create a TODO.md file to track progress:
1. Start by creating a detailed plan in TODO.md with clear steps
2. Check TODO.md before each new action to maintain context and track progress
3. Update TODO.md as you complete steps or discover new requirements
4. Mark completed items with ✓ or [x] to maintain a clear record of progress
5. For each major step, add sub-tasks as needed to break down complex work
6. If you discover the plan needs significant changes, propose updates and confirm with the user before proceeding and update TODO.md
7. IMPORTANT: Do NOT add TODO.md to git commits or version control systems
* Example TODO.md format:
```markdown
# Task: [Brief description of the overall task]
## Plan
- [ ] Step 1: [Description]
- [ ] Sub-task 1.1
- [ ] Sub-task 1.2
- [ ] Step 2: [Description]
- [x] Step 3: [Description] (Completed)
## Notes
- Important discovery: [Details about something you learned]
- Potential issue: [Description of a potential problem]
```
* When working on a task:
- Read the README to understand how the system works
- Create TODO.md with every major step unchecked
- Add TODO.md to .gitignore if it's not already ignored
- Until every item in TODO.md is checked:
a. Pick the next unchecked item and work on it
b. Run appropriate tests to verify your work
c. If issues arise, fix them until tests pass
d. Once complete, check off the item in TODO.md
e. Proceed to the next unchecked item
</TASK_MANAGEMENT>
<SECURITY>
* Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect.
* Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing.
</SECURITY>
<ENVIRONMENT_SETUP>
* When user asks you to run an application, don't stop if the application is not installed. Instead, please install the application and run the command again.
* If you encounter missing dependencies:
1. First, look around in the repository for existing dependency files (requirements.txt, pyproject.toml, package.json, Gemfile, etc.)
2. If dependency files exist, use them to install all dependencies at once (e.g., `pip install -r requirements.txt`, `npm install`, etc.)
3. Only install individual packages directly if no dependency files are found or if only specific packages are needed
* Similarly, if you encounter missing dependencies for essential tools requested by the user, install them when possible.
</ENVIRONMENT_SETUP>
<TROUBLESHOOTING>
* If you've made repeated attempts to solve a problem but tests still fail or the user reports it's still broken:
1. Step back and reflect on 5-7 different possible sources of the problem
2. Assess the likelihood of each possible cause
3. Methodically address the most likely causes, starting with the highest probability
4. Document your reasoning process
* When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
</TROUBLESHOOTING>

View File

@@ -1,6 +1,5 @@
from .bash import create_cmd_run_tool
from .browser import BrowserTool
from .condensation_request import CondensationRequestTool
from .finish import FinishTool
from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
@@ -9,7 +8,6 @@ from .think import ThinkTool
__all__ = [
'BrowserTool',
'CondensationRequestTool',
'create_cmd_run_tool',
'FinishTool',
'IPythonTool',

View File

@@ -1,16 +0,0 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk
_CONDENSATION_REQUEST_DESCRIPTION = 'Request a condensation of the conversation history when the context becomes too long or when you need to focus on the most relevant information.'
CondensationRequestTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='request_condensation',
description=_CONDENSATION_REQUEST_DESCRIPTION,
parameters={
'type': 'object',
'properties': {},
'required': [],
},
),
)

View File

@@ -77,6 +77,7 @@ async def cleanup_session(
controller: AgentController,
) -> None:
"""Clean up all resources from the current session."""
event_stream = runtime.event_stream
end_state = controller.get_state()
end_state.save_to_session(
@@ -120,7 +121,6 @@ async def run_session(
sid = generate_sid(config, session_name)
is_loaded = asyncio.Event()
is_paused = asyncio.Event() # Event to track agent pause requests
pause_task: asyncio.Task | None = None # No more than one pause task
always_confirm_mode = False # Flag to enable always confirm mode
# Show runtime initialization message
@@ -236,11 +236,9 @@ async def run_session(
if event.agent_state == AgentState.RUNNING:
display_agent_running_message()
nonlocal pause_task
if pause_task is None or pause_task.done():
pause_task = loop.create_task(
process_agent_pause(is_paused, event_stream)
) # Create a task to track agent pause requests from the user
loop.create_task(
process_agent_pause(is_paused, event_stream)
) # Create a task to track agent pause requests from the user
def on_event(event: Event) -> None:
loop.create_task(on_event_async(event))
@@ -436,23 +434,7 @@ async def main_with_loop(loop: asyncio.AbstractEventLoop) -> None:
return
# Read task from file, CLI args, or stdin
if args.file:
# For CLI usage, we want to enhance the file content with a prompt
# that instructs the agent to read and understand the file first
with open(args.file, 'r', encoding='utf-8') as file:
file_content = file.read()
# Create a prompt that instructs the agent to read and understand the file first
task_str = f"""The user has tagged a file '{args.file}'.
Please read and understand the following file content first:
```
{file_content}
```
After reviewing the file, please ask the user what they would like to do with it."""
else:
task_str = read_task(args, config.cli_multiline_input)
task_str = read_task(args, config.cli_multiline_input)
# Run the first session
new_session_requested = await run_session(

View File

@@ -59,11 +59,7 @@ from openhands.events.action import (
NullAction,
SystemMessageAction,
)
from openhands.events.action.agent import (
CondensationAction,
CondensationRequestAction,
RecallAction,
)
from openhands.events.action.agent import CondensationAction, RecallAction
from openhands.events.event import Event
from openhands.events.observation import (
AgentDelegateObservation,
@@ -74,7 +70,8 @@ from openhands.events.observation import (
)
from openhands.events.serialization.event import truncate_content
from openhands.llm.llm import LLM
from openhands.llm.metrics import Metrics
from openhands.llm.metrics import Metrics, TokenUsage
from openhands.memory.view import View
from openhands.storage.files import FileStore
# note: RESUME is only available on web GUI
@@ -339,8 +336,6 @@ class AgentController:
return True
if isinstance(event, CondensationAction):
return True
if isinstance(event, CondensationRequestAction):
return True
return False
if isinstance(event, Observation):
if (
@@ -834,9 +829,7 @@ class AgentController:
or isinstance(e, ContextWindowExceededError)
):
if self.agent.config.enable_history_truncation:
self.event_stream.add_event(
CondensationRequestAction(), EventSource.AGENT
)
self._handle_long_context_error()
return
else:
raise LLMContextWindowExceedError()
@@ -887,7 +880,7 @@ class AgentController:
action_id = getattr(action, 'id', 'unknown')
action_type = type(action).__name__
self.log(
'info',
'warning',
f'Pending action active for {elapsed_time:.2f}s: {action_type} (id={action_id})',
extra={'msg_type': 'PENDING_ACTION_TIMEOUT'},
)
@@ -956,6 +949,180 @@ class AgentController:
assert self._closed
return self.state_tracker.get_trajectory(include_screenshots)
def _handle_long_context_error(self) -> None:
# When context window is exceeded, keep roughly half of agent interactions
current_view = View.from_events(self.state.history)
kept_events = self._apply_conversation_window(current_view.events)
kept_event_ids = {e.id for e in kept_events}
self.log(
'info',
f'Context window exceeded. Keeping events with IDs: {kept_event_ids}',
)
# The events to forget are those that are not in the kept set
forgotten_event_ids = {e.id for e in self.state.history} - kept_event_ids
if len(kept_event_ids) == 0:
self.log(
'warning',
'No events kept after applying conversation window. This should not happen.',
)
# verify that the first event id in kept_event_ids is the same as the start_id
if len(kept_event_ids) > 0 and self.state.history[0].id not in kept_event_ids:
self.log(
'warning',
f'First event after applying conversation window was not kept: {self.state.history[0].id} not in {kept_event_ids}',
)
# Add an error event to trigger another step by the agent
self.event_stream.add_event(
CondensationAction(
forgotten_events_start_id=min(forgotten_event_ids)
if forgotten_event_ids
else 0,
forgotten_events_end_id=max(forgotten_event_ids)
if forgotten_event_ids
else 0,
),
EventSource.AGENT,
)
def _apply_conversation_window(self, history: list[Event]) -> list[Event]:
"""Cuts history roughly in half when context window is exceeded.
It preserves action-observation pairs and ensures that the system message,
the first user message, and its associated recall observation are always included
at the beginning of the context window.
The algorithm:
1. Identify essential initial events: System Message, First User Message, Recall Observation.
2. Determine the slice of recent events to potentially keep.
3. Validate the start of the recent slice for dangling observations.
4. Combine essential events and validated recent events, ensuring essentials come first.
Args:
events: List of events to filter
Returns:
Filtered list of events keeping newest half while preserving pairs and essential initial events.
"""
# Handle empty history
if not history:
return []
# 1. Identify essential initial events
system_message: SystemMessageAction | None = None
first_user_msg: MessageAction | None = None
recall_action: RecallAction | None = None
recall_observation: Observation | None = None
# Find System Message (should be the first event, if it exists)
system_message = next(
(e for e in history if isinstance(e, SystemMessageAction)), None
)
assert (
system_message is None
or isinstance(system_message, SystemMessageAction)
and system_message.id == history[0].id
)
# Find First User Message in the history, which MUST exist
first_user_msg = self._first_user_message(history)
if first_user_msg is None:
# If not found in history, try the event stream
first_user_msg = self._first_user_message()
if first_user_msg is None:
raise RuntimeError('No first user message found in the event stream.')
self.log(
'warning',
'First user message not found in history. Using cached version from event stream.',
)
# Find the first user message index in the history
first_user_msg_index = -1
for i, event in enumerate(history):
if isinstance(event, MessageAction) and event.source == EventSource.USER:
first_user_msg_index = i
break
# Find Recall Action and Observation related to the First User Message
# Look for RecallAction after the first user message
for i in range(first_user_msg_index + 1, len(history)):
event = history[i]
if (
isinstance(event, RecallAction)
and event.query == first_user_msg.content
):
# Found RecallAction, now look for its Observation
recall_action = event
for j in range(i + 1, len(history)):
obs_event = history[j]
# Check for Observation caused by this RecallAction
if (
isinstance(obs_event, Observation)
and obs_event.cause == recall_action.id
):
recall_observation = obs_event
break # Found the observation, stop inner loop
break # Found the recall action (and maybe obs), stop outer loop
essential_events: list[Event] = []
if system_message:
essential_events.append(system_message)
# Only include first user message if history is not empty
if history:
essential_events.append(first_user_msg)
# Include recall action and observation if both exist
if recall_action and recall_observation:
essential_events.append(recall_action)
essential_events.append(recall_observation)
# Include recall action without observation for backward compatibility
elif recall_action:
essential_events.append(recall_action)
# 2. Determine the slice of recent events to potentially keep
num_non_essential_events = len(history) - len(essential_events)
# Keep roughly half of the non-essential events, minimum 1
num_recent_to_keep = max(1, num_non_essential_events // 2)
# Calculate the starting index for the recent slice
slice_start_index = len(history) - num_recent_to_keep
slice_start_index = max(0, slice_start_index) # Ensure index is not negative
recent_events_slice = history[slice_start_index:]
# 3. Validate the start of the recent slice for dangling observations
# IMPORTANT: Most observations in history are tool call results, which cannot be without their action, or we get an LLM API error
first_valid_event_index = 0
for i, event in enumerate(recent_events_slice):
if isinstance(event, Observation):
first_valid_event_index += 1
else:
break
# If all events in the slice are dangling observations, we need to keep at least one
if first_valid_event_index == len(recent_events_slice):
self.log(
'warning',
'All recent events are dangling observations, which we truncate. This means the agent has only the essential first events. This should not happen.',
)
# Adjust the recent_events_slice if dangling observations were found at the start
if first_valid_event_index < len(recent_events_slice):
validated_recent_events = recent_events_slice[first_valid_event_index:]
if first_valid_event_index > 0:
self.log(
'debug',
f'Removed {first_valid_event_index} dangling observation(s) from the start of recent event slice.',
)
else:
validated_recent_events = []
# 4. Combine essential events and validated recent events
events_to_keep: list[Event] = essential_events + validated_recent_events
self.log('debug', f'History truncated. Kept {len(events_to_keep)} events.')
return events_to_keep
def _is_stuck(self) -> bool:
"""Checks if the agent or its delegate is stuck in a loop.
@@ -985,7 +1152,7 @@ class AgentController:
agent_metrics = self.state.metrics
# Get metrics from condenser LLM if it exists
condenser_metrics: Metrics | None = None
condenser_metrics: TokenUsage | None = None
if hasattr(self.agent, 'condenser') and hasattr(self.agent.condenser, 'llm'):
condenser_metrics = self.agent.condenser.llm.metrics

View File

@@ -31,8 +31,6 @@ class AgentConfig(BaseModel):
"""Whether to enable think tool"""
enable_finish: bool = Field(default=True)
"""Whether to enable finish tool"""
enable_condensation_request: bool = Field(default=False)
"""Whether to enable condensation request tool"""
enable_prompt_extensions: bool = Field(default=True)
"""Whether to enable prompt extensions"""
enable_mcp: bool = Field(default=True)
@@ -53,7 +51,8 @@ class AgentConfig(BaseModel):
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, AgentConfig]:
"""Create a mapping of AgentConfig instances from a toml dictionary representing the [agent] section.
"""
Create a mapping of AgentConfig instances from a toml dictionary representing the [agent] section.
The default configuration is built from all non-dict keys in data.
Then, each key with a dict value is treated as a custom agent configuration, and its values override
@@ -71,6 +70,7 @@ class AgentConfig(BaseModel):
dict[str, AgentConfig]: A mapping where the key "agent" corresponds to the default configuration
and additional keys represent custom configurations.
"""
# Initialize the result mapping
agent_mapping: dict[str, AgentConfig] = {}

View File

@@ -11,7 +11,7 @@ from openhands.core.config.llm_config import LLMConfig
class NoOpCondenserConfig(BaseModel):
"""Configuration for NoOpCondenser."""
type: Literal['noop'] = Field(default='noop')
type: Literal['noop'] = Field('noop')
model_config = ConfigDict(extra='forbid')
@@ -19,7 +19,7 @@ class NoOpCondenserConfig(BaseModel):
class ObservationMaskingCondenserConfig(BaseModel):
"""Configuration for ObservationMaskingCondenser."""
type: Literal['observation_masking'] = Field(default='observation_masking')
type: Literal['observation_masking'] = Field('observation_masking')
attention_window: int = Field(
default=100,
description='The number of most-recent events where observations will not be masked.',
@@ -32,7 +32,7 @@ class ObservationMaskingCondenserConfig(BaseModel):
class BrowserOutputCondenserConfig(BaseModel):
"""Configuration for the BrowserOutputCondenser."""
type: Literal['browser_output_masking'] = Field(default='browser_output_masking')
type: Literal['browser_output_masking'] = Field('browser_output_masking')
attention_window: int = Field(
default=1,
description='The number of most recent browser output observations that will not be masked.',
@@ -43,7 +43,7 @@ class BrowserOutputCondenserConfig(BaseModel):
class RecentEventsCondenserConfig(BaseModel):
"""Configuration for RecentEventsCondenser."""
type: Literal['recent'] = Field(default='recent')
type: Literal['recent'] = Field('recent')
# at least one event by default, because the best guess is that it is the user task
keep_first: int = Field(
@@ -61,7 +61,7 @@ class RecentEventsCondenserConfig(BaseModel):
class LLMSummarizingCondenserConfig(BaseModel):
"""Configuration for LLMCondenser."""
type: Literal['llm'] = Field(default='llm')
type: Literal['llm'] = Field('llm')
llm_config: LLMConfig = Field(
..., description='Configuration for the LLM to use for condensing.'
)
@@ -88,7 +88,7 @@ class LLMSummarizingCondenserConfig(BaseModel):
class AmortizedForgettingCondenserConfig(BaseModel):
"""Configuration for AmortizedForgettingCondenser."""
type: Literal['amortized'] = Field(default='amortized')
type: Literal['amortized'] = Field('amortized')
max_size: int = Field(
default=100,
description='Maximum size of the condensed history before triggering forgetting.',
@@ -108,7 +108,7 @@ class AmortizedForgettingCondenserConfig(BaseModel):
class LLMAttentionCondenserConfig(BaseModel):
"""Configuration for LLMAttentionCondenser."""
type: Literal['llm_attention'] = Field(default='llm_attention')
type: Literal['llm_attention'] = Field('llm_attention')
llm_config: LLMConfig = Field(
..., description='Configuration for the LLM to use for attention.'
)
@@ -131,7 +131,7 @@ class LLMAttentionCondenserConfig(BaseModel):
class StructuredSummaryCondenserConfig(BaseModel):
"""Configuration for StructuredSummaryCondenser instances."""
type: Literal['structured'] = Field(default='structured')
type: Literal['structured'] = Field('structured')
llm_config: LLMConfig = Field(
..., description='Configuration for the LLM to use for condensing.'
)
@@ -156,24 +156,16 @@ class StructuredSummaryCondenserConfig(BaseModel):
class CondenserPipelineConfig(BaseModel):
"""Configuration for the CondenserPipeline."""
type: Literal['pipeline'] = Field(default='pipeline')
condensers: list[CondenserConfig] = Field(
default_factory=list,
description='List of condenser configurations to be used in the pipeline.',
)
model_config = ConfigDict(extra='forbid')
class ConversationWindowCondenserConfig(BaseModel):
"""Configuration for ConversationWindowCondenser.
"""Configuration for the CondenserPipeline.
Not currently supported by the TOML or ENV_VAR configuration strategies.
"""
type: Literal['conversation_window'] = Field(default='conversation_window')
type: Literal['pipeline'] = Field('pipeline')
condensers: list[CondenserConfig] = Field(
default_factory=list,
description='List of condenser configurations to be used in the pipeline.',
)
model_config = ConfigDict(extra='forbid')
@@ -189,14 +181,14 @@ CondenserConfig = (
| LLMAttentionCondenserConfig
| StructuredSummaryCondenserConfig
| CondenserPipelineConfig
| ConversationWindowCondenserConfig
)
def condenser_config_from_toml_section(
data: dict, llm_configs: dict | None = None
) -> dict[str, CondenserConfig]:
"""Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
"""
Create a CondenserConfig instance from a toml dictionary representing the [condenser] section.
For CondenserConfig, the handling is different since it's a union type. The type of condenser
is determined by the 'type' field in the section.
@@ -218,6 +210,7 @@ def condenser_config_from_toml_section(
Returns:
dict[str, CondenserConfig]: A mapping where the key "condenser" corresponds to the configuration.
"""
# Initialize the result mapping
condenser_mapping: dict[str, CondenserConfig] = {}
@@ -268,7 +261,8 @@ from_toml_section = condenser_config_from_toml_section
def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
"""Create a CondenserConfig instance based on the specified type.
"""
Create a CondenserConfig instance based on the specified type.
Args:
condenser_type: The type of condenser to create.
@@ -290,9 +284,6 @@ def create_condenser_config(condenser_type: str, data: dict) -> CondenserConfig:
'amortized': AmortizedForgettingCondenserConfig,
'llm_attention': LLMAttentionCondenserConfig,
'structured': StructuredSummaryCondenserConfig,
'pipeline': CondenserPipelineConfig,
'conversation_window': ConversationWindowCondenserConfig,
'browser_output_masking': BrowserOutputCondenserConfig,
}
if condenser_type not in condenser_classes:

View File

@@ -131,9 +131,7 @@ class MCPConfig(BaseModel):
# Convert all entries in sse_servers to MCPSSEServerConfig objects
if 'sse_servers' in data:
data['sse_servers'] = cls._normalize_servers(data['sse_servers'])
servers: list[
MCPSSEServerConfig | MCPStdioServerConfig | MCPSHTTPServerConfig
] = []
servers = []
for server in data['sse_servers']:
servers.append(MCPSSEServerConfig(**server))
data['sse_servers'] = servers

View File

@@ -46,6 +46,7 @@ class OpenHandsConfig(BaseModel):
run_as_openhands: Whether to run as openhands.
max_iterations: Maximum number of iterations allowed.
max_budget_per_task: Maximum budget per task, agent stops if exceeded.
e2b_api_key: E2B API key.
disable_color: Whether to disable terminal colors. For terminals that don't support color.
debug: Whether to enable debugging mode.
file_uploads_max_file_size_mb: Maximum file upload size in MB. `0` means unlimited.
@@ -87,14 +88,19 @@ class OpenHandsConfig(BaseModel):
run_as_openhands: bool = Field(default=True)
max_iterations: int = Field(default=OH_MAX_ITERATIONS)
max_budget_per_task: float | None = Field(default=None)
e2b_api_key: SecretStr | None = Field(default=None)
modal_api_token_id: SecretStr | None = Field(default=None)
modal_api_token_secret: SecretStr | None = Field(default=None)
disable_color: bool = Field(default=False)
jwt_secret: SecretStr | None = Field(default=None)
debug: bool = Field(default=False)
file_uploads_max_file_size_mb: int = Field(default=0)
file_uploads_restrict_file_types: bool = Field(default=False)
file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*'])
runloop_api_key: SecretStr | None = Field(default=None)
daytona_api_key: SecretStr | None = Field(default=None)
daytona_api_url: str = Field(default='https://app.daytona.io/api')
daytona_target: str = Field(default='eu')
cli_multiline_input: bool = Field(default=False)
conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds
enable_default_condenser: bool = Field(default=True)

View File

@@ -261,7 +261,6 @@ class SensitiveDataFilter(logging.Filter):
'modal_api_token_secret',
'llm_api_key',
'sandbox_env_github_token',
'runloop_api_key',
'daytona_api_key',
]

View File

@@ -91,6 +91,3 @@ class ActionType(str, Enum):
CONDENSATION = 'condensation'
"""Condenses a list of events into a summary."""
CONDENSATION_REQUEST = 'condensation_request'
"""Request for condensation of a list of events."""

View File

@@ -52,6 +52,3 @@ class ObservationType(str, Enum):
MCP = 'mcp'
"""Result of a MCP Server operation"""
DOWNLOAD = 'download'
"""Result of downloading/opening a file via the browser"""

View File

@@ -112,7 +112,7 @@ def initialize_repository_for_runtime(
provider_tokens[ProviderType.BITBUCKET] = ProviderToken(token=bitbucket_token)
secret_store = (
UserSecrets(provider_tokens=provider_tokens) if provider_tokens else None # type: ignore[arg-type]
UserSecrets(provider_tokens=provider_tokens) if provider_tokens else None
)
immutable_provider_tokens = secret_store.provider_tokens if secret_store else None

View File

@@ -195,18 +195,3 @@ class CondensationAction(Action):
if self.summary:
return f'Summary: {self.summary}'
return f'Condenser is dropping the events: {self.forgotten}.'
@dataclass
class CondensationRequestAction(Action):
"""This action is used to request a condensation of the conversation history.
Attributes:
action (str): The action type, namely ActionType.CONDENSATION_REQUEST.
"""
action: str = ActionType.CONDENSATION_REQUEST
@property
def message(self) -> str:
return 'Requesting a condensation of the conversation history.'

View File

@@ -16,7 +16,6 @@ from openhands.events.observation.empty import (
NullObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.file_download import FileDownloadObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
@@ -47,5 +46,4 @@ __all__ = [
'RecallObservation',
'RecallType',
'MCPObservation',
'FileDownloadObservation',
]

View File

@@ -32,7 +32,6 @@ class BrowserOutputObservation(Observation):
last_browser_action: str = ''
last_browser_action_error: str = ''
focused_element_bid: str = ''
filter_visible_only: bool = False
@property
def message(self) -> str:

View File

@@ -1,21 +0,0 @@
from dataclasses import dataclass
from openhands.core.schema import ObservationType
from openhands.events.observation.observation import Observation
@dataclass
class FileDownloadObservation(Observation):
file_path: str
observation: str = ObservationType.DOWNLOAD
@property
def message(self) -> str:
return f'Downloaded the file at location: {self.file_path}'
def __str__(self) -> str:
ret = (
'**FileDownloadObservation**\n'
f'Location of downloaded file: {self.file_path}\n'
)
return ret

View File

@@ -9,7 +9,6 @@ from openhands.events.action.agent import (
AgentThinkAction,
ChangeAgentStateAction,
CondensationAction,
CondensationRequestAction,
RecallAction,
)
from openhands.events.action.browse import BrowseInteractiveAction, BrowseURLAction
@@ -44,7 +43,6 @@ actions = (
MessageAction,
SystemMessageAction,
CondensationAction,
CondensationRequestAction,
MCPAction,
)

View File

@@ -20,7 +20,6 @@ from openhands.events.observation.empty import (
NullObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.file_download import FileDownloadObservation
from openhands.events.observation.files import (
FileEditObservation,
FileReadObservation,
@@ -48,7 +47,6 @@ observations = (
AgentThinkObservation,
RecallObservation,
MCPObservation,
FileDownloadObservation,
)
OBSERVATION_TYPE_TO_CLASS = {

View File

@@ -1,5 +1,4 @@
import base64
import os
from typing import Any
import httpx
@@ -16,10 +15,9 @@ from openhands.integrations.service_types import (
User,
)
from openhands.server.types import AppMode
from openhands.utils.import_utils import get_impl
class BitBucketService(BaseGitService, GitService):
class BitbucketService(BaseGitService, GitService):
"""Default implementation of GitService for Bitbucket integration.
This is an extension point in OpenHands that allows applications to customize Bitbucket
@@ -149,41 +147,6 @@ class BitBucketService(BaseGitService, GitService):
# Bitbucket doesn't have a dedicated search endpoint like GitHub
return []
async def _fetch_paginated_data(
self, url: str, params: dict, max_items: int
) -> list[dict]:
"""
Fetch data with pagination support for Bitbucket API.
Args:
url: The API endpoint URL
params: Query parameters for the request
max_items: Maximum number of items to fetch
Returns:
List of data items from all pages
"""
all_items: list[dict] = []
current_url = url
while current_url and len(all_items) < max_items:
response, _ = await self._make_request(current_url, params)
# Extract items from response
page_items = response.get('values', [])
if not page_items: # No more items
break
all_items.extend(page_items)
# Get the next page URL from the response
current_url = response.get('next')
# Clear params for subsequent requests since the next URL already contains all parameters
params = {}
return all_items[:max_items] # Trim to max_items if needed
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
"""Get repositories for the authenticated user using workspaces endpoint.
@@ -192,51 +155,33 @@ class BitBucketService(BaseGitService, GitService):
This approach is more comprehensive and efficient than the previous implementation
that made separate calls for public and private repositories.
"""
MAX_REPOS = 1000
PER_PAGE = 100 # Maximum allowed by Bitbucket API
repositories: list[Repository] = []
repositories = []
# Get user's workspaces with pagination
# Get user's workspaces
workspaces_url = f'{self.BASE_URL}/workspaces'
workspaces = await self._fetch_paginated_data(workspaces_url, {}, MAX_REPOS)
workspaces_data, _ = await self._make_request(workspaces_url)
for workspace in workspaces:
for workspace in workspaces_data.get('values', []):
workspace_slug = workspace.get('slug')
if not workspace_slug:
continue
# Get repositories for this workspace with pagination
# Get repositories for this workspace
workspace_repos_url = f'{self.BASE_URL}/repositories/{workspace_slug}'
# Map sort parameter to Bitbucket API compatible values and ensure descending order
# to show most recently changed repos at the top
# Map sort parameter to Bitbucket API compatible values
bitbucket_sort = sort
if sort == 'pushed':
# Bitbucket doesn't support 'pushed', use 'updated_on' instead
bitbucket_sort = (
'-updated_on' # Use negative prefix for descending order
)
elif sort == 'updated':
bitbucket_sort = '-updated_on'
elif sort == 'created':
bitbucket_sort = '-created_on'
elif sort == 'full_name':
bitbucket_sort = 'name' # Bitbucket uses 'name' not 'full_name'
else:
# Default to most recently updated first
bitbucket_sort = '-updated_on'
bitbucket_sort = 'updated_on'
params = {
'pagelen': PER_PAGE,
'pagelen': 100,
'sort': bitbucket_sort,
}
repos_data, headers = await self._make_request(workspace_repos_url, params)
# Fetch all repositories for this workspace with pagination
workspace_repos = await self._fetch_paginated_data(
workspace_repos_url, params, MAX_REPOS - len(repositories)
)
for repo in workspace_repos:
for repo in repos_data.get('values', []):
uuid = repo.get('uuid', '')
repositories.append(
Repository(
@@ -245,18 +190,11 @@ class BitBucketService(BaseGitService, GitService):
git_provider=ProviderType.BITBUCKET,
is_public=repo.get('is_private', True) is False,
stargazers_count=None, # Bitbucket doesn't have stars
link_header=headers.get('Link', ''),
pushed_at=repo.get('updated_on'),
)
)
# Stop if we've reached the maximum number of repositories
if len(repositories) >= MAX_REPOS:
break
# Stop if we've reached the maximum number of repositories
if len(repositories) >= MAX_REPOS:
break
return repositories
async def get_suggested_tasks(self) -> list[SuggestedTask]:
@@ -300,21 +238,10 @@ class BitBucketService(BaseGitService, GitService):
repo = parts[-1]
url = f'{self.BASE_URL}/repositories/{owner}/{repo}/refs/branches'
# Set maximum branches to fetch (similar to GitHub/GitLab implementations)
MAX_BRANCHES = 1000
PER_PAGE = 100
params = {
'pagelen': PER_PAGE,
'sort': '-target.date', # Sort by most recent commit date, descending
}
# Fetch all branches with pagination
branch_data = await self._fetch_paginated_data(url, params, MAX_BRANCHES)
data, _ = await self._make_request(url)
branches = []
for branch in branch_data:
for branch in data.get('values', []):
branches.append(
Branch(
name=branch.get('name', ''),
@@ -373,10 +300,3 @@ class BitBucketService(BaseGitService, GitService):
# Return the URL to the pull request
return data.get('links', {}).get('html', {}).get('href', '')
bitbucket_service_cls = os.environ.get(
'OPENHANDS_BITBUCKET_SERVICE_CLS',
'openhands.integrations.bitbucket.bitbucket_service.BitBucketService',
)
BitBucketServiceImpl = get_impl(BitBucketService, bitbucket_service_cls)

View File

@@ -72,9 +72,7 @@ class GitHubService(BaseGitService, GitService):
async def _get_github_headers(self) -> dict:
"""Retrieve the GH Token from settings store to construct the headers."""
if not self.token:
latest_token = await self.get_latest_token()
if latest_token:
self.token = latest_token
self.token = await self.get_latest_token()
return {
'Authorization': f'Bearer {self.token.get_secret_value() if self.token else ""}',
@@ -231,8 +229,8 @@ class GitHubService(BaseGitService, GitService):
# Convert to Repository objects
return [
Repository(
id=str(repo.get('id')), # type: ignore[arg-type]
full_name=repo.get('full_name'), # type: ignore[arg-type]
id=str(repo.get('id')),
full_name=repo.get('full_name'),
stargazers_count=repo.get('stargazers_count'),
git_provider=ProviderType.GITHUB,
is_public=not repo.get('private', True),

View File

@@ -66,9 +66,7 @@ class GitLabService(BaseGitService, GitService):
Retrieve the GitLab Token to construct the headers
"""
if not self.token:
latest_token = await self.get_latest_token()
if latest_token:
self.token = latest_token
self.token = await self.get_latest_token()
return {
'Authorization': f'Bearer {self.token.get_secret_value()}',
@@ -187,7 +185,7 @@ class GitLabService(BaseGitService, GitService):
return User(
id=str(response.get('id', '')),
login=response.get('username'), # type: ignore[call-arg]
login=response.get('username'),
avatar_url=avatar_url,
name=response.get('name'),
email=response.get('email'),
@@ -260,8 +258,8 @@ class GitLabService(BaseGitService, GitService):
all_repos = all_repos[:MAX_REPOS]
return [
Repository(
id=str(repo.get('id')), # type: ignore[arg-type]
full_name=repo.get('path_with_namespace'), # type: ignore[arg-type]
id=str(repo.get('id')),
full_name=repo.get('path_with_namespace'),
stargazers_count=repo.get('star_count'),
git_provider=ProviderType.GITLAB,
is_public=repo.get('visibility') == 'public',

View File

@@ -15,7 +15,7 @@ from openhands.core.logger import openhands_logger as logger
from openhands.events.action.action import Action
from openhands.events.action.commands import CmdRunAction
from openhands.events.stream import EventStream
from openhands.integrations.bitbucket.bitbucket_service import BitBucketServiceImpl
from openhands.integrations.bitbucket.bitbucket_service import BitbucketService
from openhands.integrations.github.github_service import GithubServiceImpl
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
from openhands.integrations.service_types import (
@@ -50,7 +50,7 @@ class ProviderToken(BaseModel):
# Override with emtpy string if it was set to None
# Cannot pass None to SecretStr
if token_str is None:
token_str = '' # type: ignore[unreachable]
token_str = ''
user_id = token_value.get('user_id')
host = token_value.get('host')
return cls(token=SecretStr(token_str), user_id=user_id, host=host)
@@ -74,8 +74,8 @@ class CustomSecret(BaseModel):
if isinstance(secret_value, CustomSecret):
return secret_value
elif isinstance(secret_value, dict):
secret = secret_value.get('secret', '')
description = secret_value.get('description', '')
secret = secret_value.get('secret')
description = secret_value.get('description')
return cls(secret=SecretStr(secret), description=description)
else:
@@ -110,7 +110,7 @@ class ProviderHandler:
self.service_class_map: dict[ProviderType, type[GitService]] = {
ProviderType.GITHUB: GithubServiceImpl,
ProviderType.GITLAB: GitLabServiceImpl,
ProviderType.BITBUCKET: BitBucketServiceImpl,
ProviderType.BITBUCKET: BitbucketService,
}
self.external_auth_id = external_auth_id

View File

@@ -1,7 +1,7 @@
from pydantic import SecretStr
from openhands.core.logger import openhands_logger as logger
from openhands.integrations.bitbucket.bitbucket_service import BitBucketService
from openhands.integrations.bitbucket.bitbucket_service import BitbucketService
from openhands.integrations.github.github_service import GitHubService
from openhands.integrations.gitlab.gitlab_service import GitLabService
from openhands.integrations.provider import ProviderType
@@ -26,7 +26,7 @@ async def validate_provider_token(
"""
# Skip validation for empty tokens
if token is None:
return None # type: ignore[unreachable]
return None
# Try GitHub first
github_error = None
@@ -49,7 +49,7 @@ async def validate_provider_token(
# Try Bitbucket last
bitbucket_error = None
try:
bitbucket_service = BitBucketService(token=token, base_domain=base_domain)
bitbucket_service = BitbucketService(token=token, base_domain=base_domain)
await bitbucket_service.get_user()
return ProviderType.BITBUCKET
except Exception as e:

View File

@@ -19,7 +19,6 @@ from litellm import completion as litellm_completion
from litellm import completion_cost as litellm_completion_cost
from litellm.exceptions import (
RateLimitError,
ServiceUnavailableError,
)
from litellm.types.utils import CostPerToken, ModelResponse, Usage
from litellm.utils import create_pretrained_tokenizer
@@ -41,7 +40,6 @@ __all__ = ['LLM']
# tuple of exceptions to retry on
LLM_RETRY_EXCEPTIONS: tuple[type[Exception], ...] = (
RateLimitError,
ServiceUnavailableError,
litellm.Timeout,
litellm.InternalServerError,
LLMNoResponseError,
@@ -165,6 +163,7 @@ class LLM(RetryMixin, DebugMixin):
'temperature': self.config.temperature,
'max_completion_tokens': self.config.max_output_tokens,
}
if self.config.top_k is not None:
# openai doesn't expose top_k
# litellm will handle it a bit differently than the openai-compatible params
@@ -494,26 +493,6 @@ class LLM(RetryMixin, DebugMixin):
# Safe fallback for any potentially viable model
self.config.max_input_tokens = 4096
if self.config.max_output_tokens is None:
# Safe default for any potentially viable model
self.config.max_output_tokens = 4096
if self.model_info is not None:
# max_output_tokens has precedence over max_tokens, if either exists.
# litellm has models with both, one or none of these 2 parameters!
if 'max_output_tokens' in self.model_info and isinstance(
self.model_info['max_output_tokens'], int
):
self.config.max_output_tokens = self.model_info['max_output_tokens']
elif 'max_tokens' in self.model_info and isinstance(
self.model_info['max_tokens'], int
):
self.config.max_output_tokens = self.model_info['max_tokens']
if any(
model in self.config.model
for model in ['claude-3-7-sonnet', 'claude-3.7-sonnet']
):
self.config.max_output_tokens = 64000 # litellm set max to 128k, but that requires a header to be set
# Initialize function calling capability
# Check if model name is in our supported list
model_name_supported = (

View File

@@ -62,10 +62,8 @@ async def create_mcp_clients(
)
return []
servers: list[MCPSSEServerConfig | MCPSHTTPServerConfig] = [
*sse_servers,
*shttp_servers,
]
servers: list[MCPSSEServerConfig | MCPSHTTPServerConfig] = sse_servers.copy()
servers.extend(shttp_servers.copy())
if not servers:
return []

View File

@@ -4,9 +4,6 @@ from openhands.memory.condenser.impl.amortized_forgetting_condenser import (
from openhands.memory.condenser.impl.browser_output_condenser import (
BrowserOutputCondenser,
)
from openhands.memory.condenser.impl.conversation_window_condenser import (
ConversationWindowCondenser,
)
from openhands.memory.condenser.impl.llm_attention_condenser import (
ImportantEventSelection,
LLMAttentionCondenser,
@@ -37,5 +34,4 @@ __all__ = [
'RecentEventsCondenser',
'StructuredSummaryCondenser',
'CondenserPipeline',
'ConversationWindowCondenser',
]

View File

@@ -60,7 +60,7 @@ class AmortizedForgettingCondenser(RollingCondenser):
def from_config(
cls, config: AmortizedForgettingCondenserConfig
) -> AmortizedForgettingCondenser:
return AmortizedForgettingCondenser(**config.model_dump(exclude={'type'}))
return AmortizedForgettingCondenser(**config.model_dump(exclude=['type']))
AmortizedForgettingCondenser.register_config(AmortizedForgettingCondenserConfig)

View File

@@ -42,7 +42,7 @@ class BrowserOutputCondenser(Condenser):
def from_config(
cls, config: BrowserOutputCondenserConfig
) -> BrowserOutputCondenser:
return BrowserOutputCondenser(**config.model_dump(exclude={'type'}))
return BrowserOutputCondenser(**config.model_dump(exclude=['type']))
BrowserOutputCondenser.register_config(BrowserOutputCondenserConfig)

View File

@@ -1,185 +0,0 @@
from __future__ import annotations
from openhands.core.config.condenser_config import ConversationWindowCondenserConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.agent import (
CondensationAction,
RecallAction,
)
from openhands.events.action.message import MessageAction, SystemMessageAction
from openhands.events.event import EventSource
from openhands.events.observation import Observation
from openhands.memory.condenser.condenser import Condensation, RollingCondenser, View
class ConversationWindowCondenser(RollingCondenser):
def __init__(self) -> None:
super().__init__()
def get_condensation(self, view: View) -> Condensation:
"""Apply conversation window truncation similar to _apply_conversation_window.
This method:
1. Identifies essential initial events (System Message, First User Message, Recall Observation)
2. Keeps roughly half of the history
3. Ensures action-observation pairs are preserved
4. Returns a CondensationAction specifying which events to forget
"""
events = view.events
# Handle empty history
if not events:
# No events to condense
action = CondensationAction(forgotten_event_ids=[])
return Condensation(action=action)
# 1. Identify essential initial events
system_message: SystemMessageAction | None = None
first_user_msg: MessageAction | None = None
recall_action: RecallAction | None = None
recall_observation: Observation | None = None
# Find System Message (should be the first event, if it exists)
system_message = next(
(e for e in events if isinstance(e, SystemMessageAction)), None
)
# Find First User Message
first_user_msg = next(
(
e
for e in events
if isinstance(e, MessageAction) and e.source == EventSource.USER
),
None,
)
if first_user_msg is None:
logger.warning(
'No first user message found in history during condensation.'
)
# Return empty condensation if no user message
action = CondensationAction(forgotten_event_ids=[])
return Condensation(action=action)
# Find the first user message index
first_user_msg_index = -1
for i, event in enumerate(events):
if isinstance(event, MessageAction) and event.source == EventSource.USER:
first_user_msg_index = i
break
# Find Recall Action and Observation related to the First User Message
for i in range(first_user_msg_index + 1, len(events)):
event = events[i]
if (
isinstance(event, RecallAction)
and event.query == first_user_msg.content
):
recall_action = event
# Look for its observation
for j in range(i + 1, len(events)):
obs_event = events[j]
if (
isinstance(obs_event, Observation)
and obs_event.cause == recall_action.id
):
recall_observation = obs_event
break
break
# Collect essential events
essential_events: list[int] = [] # Store event IDs
if system_message:
essential_events.append(system_message.id)
essential_events.append(first_user_msg.id)
if recall_action:
essential_events.append(recall_action.id)
if recall_observation:
essential_events.append(recall_observation.id)
# 2. Determine which events to keep
num_essential_events = len(essential_events)
total_events = len(events)
num_non_essential_events = total_events - num_essential_events
# Keep roughly half of the non-essential events
num_recent_to_keep = max(1, num_non_essential_events // 2)
# Calculate the starting index for recent events to keep
slice_start_index = total_events - num_recent_to_keep
slice_start_index = max(0, slice_start_index)
# 3. Handle dangling observations at the start of the slice
# Find the first non-observation event in the slice
recent_events_slice = events[slice_start_index:]
first_valid_event_index_in_slice = 0
for i, event in enumerate(recent_events_slice):
if not isinstance(event, Observation):
first_valid_event_index_in_slice = i
break
else:
# All events in the slice are observations
first_valid_event_index_in_slice = len(recent_events_slice)
# Check if all events in the recent slice are dangling observations
if first_valid_event_index_in_slice == len(recent_events_slice):
logger.warning(
'All recent events are dangling observations, which we truncate. This means the agent has only the essential first events. This should not happen.'
)
# Calculate the actual index in the full events list
first_valid_event_index = slice_start_index + first_valid_event_index_in_slice
if first_valid_event_index_in_slice > 0:
logger.debug(
f'Removed {first_valid_event_index_in_slice} dangling observation(s) '
f'from the start of recent event slice.'
)
# 4. Determine which events to keep and which to forget
events_to_keep: set[int] = set(essential_events)
# Add recent events starting from first_valid_event_index
for i in range(first_valid_event_index, total_events):
events_to_keep.add(events[i].id)
# Calculate which events to forget
all_event_ids = {e.id for e in events}
forgotten_event_ids = sorted(all_event_ids - events_to_keep)
logger.info(
f'ConversationWindowCondenser: Keeping {len(events_to_keep)} events, '
f'forgetting {len(forgotten_event_ids)} events.'
)
# Create the condensation action
if forgotten_event_ids:
# Use range if the forgotten events are contiguous
if (
len(forgotten_event_ids) > 1
and forgotten_event_ids[-1] - forgotten_event_ids[0]
== len(forgotten_event_ids) - 1
):
action = CondensationAction(
forgotten_events_start_id=forgotten_event_ids[0],
forgotten_events_end_id=forgotten_event_ids[-1],
)
else:
action = CondensationAction(forgotten_event_ids=forgotten_event_ids)
else:
action = CondensationAction(forgotten_event_ids=[])
return Condensation(action=action)
def should_condense(self, view: View) -> bool:
return view.unhandled_condensation_request
@classmethod
def from_config(
cls, _config: ConversationWindowCondenserConfig
) -> ConversationWindowCondenser:
return ConversationWindowCondenser()
ConversationWindowCondenser.register_config(ConversationWindowCondenserConfig)

View File

@@ -30,7 +30,7 @@ class ObservationMaskingCondenser(Condenser):
def from_config(
cls, config: ObservationMaskingCondenserConfig
) -> ObservationMaskingCondenser:
return ObservationMaskingCondenser(**config.model_dump(exclude={'type'}))
return ObservationMaskingCondenser(**config.model_dump(exclude=['type']))
ObservationMaskingCondenser.register_config(ObservationMaskingCondenserConfig)

View File

@@ -22,7 +22,7 @@ class RecentEventsCondenser(Condenser):
@classmethod
def from_config(cls, config: RecentEventsCondenserConfig) -> RecentEventsCondenser:
return RecentEventsCondenser(**config.model_dump(exclude={'type'}))
return RecentEventsCondenser(**config.model_dump(exclude=['type']))
RecentEventsCondenser.register_config(RecentEventsCondenserConfig)

View File

@@ -28,7 +28,6 @@ from openhands.events.observation import (
AgentThinkObservation,
BrowserOutputObservation,
CmdOutputObservation,
FileDownloadObservation,
FileEditObservation,
FileReadObservation,
IPythonRunCellObservation,
@@ -289,12 +288,7 @@ class ConversationMemory:
role = 'user' if action.source == 'user' else 'assistant'
content = [TextContent(text=action.content or '')]
if vision_is_active and action.image_urls:
if role == 'user':
for idx, url in enumerate(action.image_urls):
content.append(TextContent(text=f'Image {idx + 1}:'))
content.append(ImageContent(image_urls=[url]))
else:
content.append(ImageContent(image_urls=action.image_urls))
content.append(ImageContent(image_urls=action.image_urls))
if role not in ('user', 'system', 'assistant', 'tool'):
raise ValueError(f'Invalid role: {role}')
return [
@@ -345,7 +339,6 @@ class ConversationMemory:
- AgentDelegateObservation: Formats results from delegated agent tasks
- ErrorObservation: Formats error messages from failed actions
- UserRejectObservation: Formats user rejection messages
- FileDownloadObservation: Formats the result of a browsing action that opened/downloaded a file
In function calling mode, observations with tool_call_metadata are stored in
tool_call_id_to_message for later processing instead of being returned immediately.
@@ -395,7 +388,7 @@ class ConversationMemory:
text = truncate_content(text, max_message_chars)
# Create message content with text
content: list[TextContent | ImageContent] = [TextContent(text=text)]
content = [TextContent(text=text)]
# Add image URLs if available and vision is active
if vision_is_active and obs.image_urls:
@@ -411,7 +404,7 @@ class ConversationMemory:
# Add text indicating some images were filtered
content[
0
].text += f'\n\nNote: {invalid_count} invalid or empty image(s) were filtered from this output. The agent may need to use alternative methods to access visual information.' # type: ignore[union-attr]
].text += f'\n\nNote: {invalid_count} invalid or empty image(s) were filtered from this output. The agent may need to use alternative methods to access visual information.'
else:
logger.debug(
'IPython observation has image URLs but none are valid'
@@ -419,7 +412,7 @@ class ConversationMemory:
# Add text indicating all images were filtered
content[
0
].text += f'\n\nNote: All {len(obs.image_urls)} image(s) in this output were invalid or empty and have been filtered. The agent should use alternative methods to access visual information.' # type: ignore[union-attr]
].text += f'\n\nNote: All {len(obs.image_urls)} image(s) in this output were invalid or empty and have been filtered. The agent should use alternative methods to access visual information.'
message = Message(role='user', content=content)
elif isinstance(obs, FileEditObservation):
@@ -436,7 +429,7 @@ class ConversationMemory:
and enable_som_visual_browsing
and vision_is_active
):
text += 'Image: Current webpage screenshot (Note that only visible portion of webpage is present in the screenshot. However, the Accessibility tree contains information from the entire webpage.)\n'
text += 'Image: Current webpage screenshot (Note that only visible portion of webpage is present in the screenshot. You may need to scroll to view the remaining portion of the web-page.)\n'
# Determine which image to use and validate it
image_url = None
@@ -452,7 +445,7 @@ class ConversationMemory:
# Only add ImageContent if we have a valid image URL
if self._is_valid_image_url(image_url):
content.append(ImageContent(image_urls=[image_url])) # type: ignore[list-item]
content.append(ImageContent(image_urls=[image_url]))
logger.debug(f'Vision enabled for browsing, showing {image_type}')
else:
if image_url:
@@ -462,7 +455,7 @@ class ConversationMemory:
# Add text indicating the image was filtered
content[
0
].text += f'\n\nNote: The {image_type} for this webpage was invalid or empty and has been filtered. The agent should use alternative methods to access visual information about the webpage.' # type: ignore[union-attr]
].text += f'\n\nNote: The {image_type} for this webpage was invalid or empty and has been filtered. The agent should use alternative methods to access visual information about the webpage.'
else:
logger.debug(
'Vision enabled for browsing, but no valid image available'
@@ -470,7 +463,7 @@ class ConversationMemory:
# Add text indicating no image was available
content[
0
].text += '\n\nNote: No visual information (screenshot or set of marks) is available for this webpage. The agent should rely on the text content above.' # type: ignore[union-attr]
].text += '\n\nNote: No visual information (screenshot or set of marks) is available for this webpage. The agent should rely on the text content above.'
message = Message(role='user', content=content)
else:
@@ -499,9 +492,6 @@ class ConversationMemory:
elif isinstance(obs, AgentCondensationObservation):
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, FileDownloadObservation):
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif (
isinstance(obs, RecallObservation)
and self.agent_config.enable_prompt_extensions
@@ -565,7 +555,7 @@ class ConversationMemory:
has_microagent_knowledge = bool(filtered_agents)
# Generate appropriate content based on what is present
message_content: list[TextContent | ImageContent] = []
message_content = []
# Build the workspace context information
if (

View File

@@ -2,7 +2,6 @@ import asyncio
import os
import uuid
from datetime import datetime, timezone
from pathlib import Path
from typing import Callable
import openhands
@@ -34,8 +33,6 @@ GLOBAL_MICROAGENTS_DIR = os.path.join(
'microagents',
)
USER_MICROAGENTS_DIR = Path.home() / '.openhands' / 'microagents'
class Memory:
"""
@@ -47,8 +44,6 @@ class Memory:
event_stream: EventStream
status_callback: Callable | None
loop: asyncio.AbstractEventLoop | None
repo_microagents: dict[str, RepoMicroagent]
knowledge_microagents: dict[str, KnowledgeMicroagent]
def __init__(
self,
@@ -68,8 +63,8 @@ class Memory:
)
# Additional placeholders to store user workspace microagents
self.repo_microagents = {}
self.knowledge_microagents = {}
self.repo_microagents: dict[str, RepoMicroagent] = {}
self.knowledge_microagents: dict[str, KnowledgeMicroagent] = {}
# Store repository / runtime info to send them to the templating later
self.repository_info: RepositoryInfo | None = None
@@ -80,9 +75,6 @@ class Memory:
# from typically OpenHands/microagents (i.e., the PUBLIC microagents)
self._load_global_microagents()
# Load user microagents from ~/.openhands/microagents/
self._load_user_microagents()
def on_event(self, event: Event):
"""Handle an event from the event stream."""
asyncio.get_event_loop().run_until_complete(self._on_event(event))
@@ -273,34 +265,12 @@ class Memory:
repo_agents, knowledge_agents = load_microagents_from_dir(
GLOBAL_MICROAGENTS_DIR
)
for name, agent_knowledge in knowledge_agents.items():
self.knowledge_microagents[name] = agent_knowledge
for name, agent_repo in repo_agents.items():
self.repo_microagents[name] = agent_repo
def _load_user_microagents(self) -> None:
"""
Loads microagents from the user's home directory (~/.openhands/microagents/)
Creates the directory if it doesn't exist.
"""
try:
# Create the user microagents directory if it doesn't exist
os.makedirs(USER_MICROAGENTS_DIR, exist_ok=True)
# Load microagents from user directory
repo_agents, knowledge_agents = load_microagents_from_dir(
USER_MICROAGENTS_DIR
)
for name, agent_knowledge in knowledge_agents.items():
self.knowledge_microagents[name] = agent_knowledge
for name, agent_repo in repo_agents.items():
self.repo_microagents[name] = agent_repo
except Exception as e:
logger.warning(
f'Failed to load user microagents from {USER_MICROAGENTS_DIR}: {str(e)}'
)
for name, agent in knowledge_agents.items():
if isinstance(agent, KnowledgeMicroagent):
self.knowledge_microagents[name] = agent
for name, agent in repo_agents.items():
if isinstance(agent, RepoMicroagent):
self.repo_microagents[name] = agent
def get_microagent_mcp_tools(self) -> list[MCPConfig]:
"""

View File

@@ -5,7 +5,7 @@ from typing import overload
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.agent import CondensationAction, CondensationRequestAction
from openhands.events.action.agent import CondensationAction
from openhands.events.event import Event
from openhands.events.observation.agent import AgentCondensationObservation
@@ -17,7 +17,6 @@ class View(BaseModel):
"""
events: list[Event]
unhandled_condensation_request: bool = False
def __len__(self) -> int:
return len(self.events)
@@ -53,8 +52,6 @@ class View(BaseModel):
forgotten_event_ids.update(event.forgotten)
# Make sure we also forget the condensation action itself
forgotten_event_ids.add(event.id)
if isinstance(event, CondensationRequestAction):
forgotten_event_ids.add(event.id)
kept_events = [event for event in events if event.id not in forgotten_event_ids]
@@ -77,17 +74,4 @@ class View(BaseModel):
summary_offset, AgentCondensationObservation(content=summary)
)
# Check for an unhandled condensation request -- these are events closer to the
# end of the list than any condensation action.
unhandled_condensation_request = False
for event in reversed(events):
if isinstance(event, CondensationAction):
break
if isinstance(event, CondensationRequestAction):
unhandled_condensation_request = True
break
return View(
events=kept_events,
unhandled_condensation_request=unhandled_condensation_request,
)
return View(events=kept_events)

View File

@@ -1,6 +1,5 @@
import io
import re
from itertools import chain
from pathlib import Path
from typing import Union
@@ -40,11 +39,7 @@ class BaseMicroagent(BaseModel):
# Otherwise, we will rely on the name from metadata later
derived_name = None
if microagent_dir is not None:
# Special handling for .cursorrules files which are not in microagent_dir
if path.name == '.cursorrules':
derived_name = 'cursorrules'
else:
derived_name = str(path.relative_to(microagent_dir).with_suffix(''))
derived_name = str(path.relative_to(microagent_dir).with_suffix(''))
# Only load directly from path if file_content is not provided
if file_content is None:
@@ -61,16 +56,6 @@ class BaseMicroagent(BaseModel):
type=MicroagentType.REPO_KNOWLEDGE,
)
# Handle .cursorrules files
if path.name == '.cursorrules':
return RepoMicroagent(
name='cursorrules',
content=file_content,
metadata=MicroagentMetadata(name='cursorrules'),
source=str(path),
type=MicroagentType.REPO_KNOWLEDGE,
)
file_io = io.StringIO(file_content)
loaded = frontmatter.load(file_io)
content = loaded.content
@@ -273,15 +258,10 @@ def load_microagents_from_dir(
# Load all agents from microagents directory
logger.debug(f'Loading agents from {microagent_dir}')
if microagent_dir.exists():
# Collect .cursorrules file from repo root and .md files from microagents dir
cursorrules_files = []
if (microagent_dir.parent.parent / '.cursorrules').exists():
cursorrules_files = [microagent_dir.parent.parent / '.cursorrules']
md_files = [f for f in microagent_dir.rglob('*.md') if f.name != 'README.md']
# Process all files in one loop
for file in chain(cursorrules_files, md_files):
for file in microagent_dir.rglob('*.md'):
# skip README.md
if file.name == 'README.md':
continue
try:
agent = BaseMicroagent.load(file, microagent_dir)
if isinstance(agent, RepoMicroagent):

View File

@@ -5,6 +5,7 @@ import dataclasses
import json
import os
import pathlib
import shutil
import subprocess
from argparse import Namespace
from typing import Any
@@ -393,7 +394,7 @@ class IssueResolver:
async def process_issue(
self,
issue: Issue,
branch_to_checkout: str | None,
base_commit: str,
issue_handler: ServiceContextIssue | ServiceContextPR,
reset_logger: bool = False,
) -> ResolverOutput:
@@ -404,45 +405,14 @@ class IssueResolver:
else:
logger.info(f'Starting fixing issue {issue.number}.')
# create runtime and clone repo using standard pattern
# write the repo to the workspace
if os.path.exists(self.workspace_base):
shutil.rmtree(self.workspace_base)
shutil.copytree(os.path.join(self.output_dir, 'repo'), self.workspace_base)
runtime = create_runtime(self.app_config)
await runtime.connect()
# clone repo directly into runtime workspace
from openhands.core.setup import initialize_repository_for_runtime
initialize_repository_for_runtime(runtime, self.issue_handler.get_clone_url())
# checkout to PR branch if needed
if branch_to_checkout:
logger.info(f'Checking out to PR branch {branch_to_checkout}')
# Fetch the branch first to ensure it exists locally
fetch_cmd = ['git', 'fetch', 'origin', branch_to_checkout]
subprocess.check_output(fetch_cmd, cwd=runtime.workspace_root) # noqa: ASYNC101
# Checkout the branch
checkout_cmd = ['git', 'checkout', branch_to_checkout]
subprocess.check_output(checkout_cmd, cwd=runtime.workspace_root) # noqa: ASYNC101
# get the commit id of current repo for reproducibility
base_commit = (
subprocess.check_output(
['git', 'rev-parse', 'HEAD'], cwd=runtime.workspace_root
) # noqa: ASYNC101
.decode('utf-8')
.strip()
)
logger.info(f'Base commit: {base_commit}')
# Check for .openhands_instructions file in the workspace directory
if self.repo_instruction is None:
openhands_instructions_path = os.path.join(
runtime.workspace_root, '.openhands_instructions'
)
if os.path.exists(openhands_instructions_path):
with open(openhands_instructions_path, 'r') as f: # noqa: ASYNC101
self.repo_instruction = f.read()
def on_event(evt: Event) -> None:
logger.info(evt)
@@ -595,10 +565,36 @@ class IssueResolver:
)
logger.info(f'Using output directory: {self.output_dir}')
# repo will be cloned later in process_issue using standard pattern
# base_commit will be captured after cloning
# checkout the repo
repo_dir = os.path.join(self.output_dir, 'repo')
if not os.path.exists(repo_dir):
checkout_output = subprocess.check_output( # noqa: ASYNC101
[
'git',
'clone',
self.issue_handler.get_clone_url(),
f'{self.output_dir}/repo',
]
).decode('utf-8')
if 'fatal' in checkout_output:
raise RuntimeError(f'Failed to clone repository: {checkout_output}')
# .openhands_instructions will be read after repo is cloned in process_issue
# get the commit id of current repo for reproducibility
base_commit = (
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir) # noqa: ASYNC101
.decode('utf-8')
.strip()
)
logger.info(f'Base commit: {base_commit}')
if self.repo_instruction is None:
# Check for .openhands_instructions file in the workspace directory
openhands_instructions_path = os.path.join(
repo_dir, '.openhands_instructions'
)
if os.path.exists(openhands_instructions_path):
with open(openhands_instructions_path, 'r') as f: # noqa: ASYNC101
self.repo_instruction = f.read()
# OUTPUT FILE
output_file = os.path.join(self.output_dir, 'output.jsonl')
@@ -622,19 +618,39 @@ class IssueResolver:
)
try:
# determine branch to use for PR
branch_to_use = None
# checkout to pr branch if needed
if self.issue_type == 'pr':
branch_to_use = issue.head_branch
logger.info(
f'Will checkout to PR branch {branch_to_use} for issue {issue.number}'
f'Checking out to PR branch {branch_to_use} for issue {issue.number}'
)
if not branch_to_use:
raise ValueError('Branch name cannot be None')
# Fetch the branch first to ensure it exists locally
fetch_cmd = ['git', 'fetch', 'origin', branch_to_use]
subprocess.check_output( # noqa: ASYNC101
fetch_cmd,
cwd=repo_dir,
)
# Checkout the branch
checkout_cmd = ['git', 'checkout', branch_to_use]
subprocess.check_output( # noqa: ASYNC101
checkout_cmd,
cwd=repo_dir,
)
base_commit = (
subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=repo_dir) # noqa: ASYNC101
.decode('utf-8')
.strip()
)
output = await self.process_issue(
issue,
branch_to_use, # pass branch instead of base_commit
base_commit,
self.issue_handler,
reset_logger,
)

View File

@@ -1,91 +1,31 @@
import importlib
from openhands.runtime.base import Runtime
from openhands.runtime.impl.cli.cli_runtime import CLIRuntime
from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime
from openhands.runtime.impl.docker.docker_runtime import (
DockerRuntime,
)
from openhands.runtime.impl.e2b.e2b_runtime import E2BRuntime
from openhands.runtime.impl.kubernetes.kubernetes_runtime import KubernetesRuntime
from openhands.runtime.impl.local.local_runtime import LocalRuntime
from openhands.runtime.impl.modal.modal_runtime import ModalRuntime
from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime
from openhands.utils.import_utils import get_impl
# mypy: disable-error-code="type-abstract"
_DEFAULT_RUNTIME_CLASSES: dict[str, type[Runtime]] = {
'eventstream': DockerRuntime,
'docker': DockerRuntime,
'e2b': E2BRuntime,
'remote': RemoteRuntime,
'modal': ModalRuntime,
'runloop': RunloopRuntime,
'local': LocalRuntime,
'daytona': DaytonaRuntime,
'kubernetes': KubernetesRuntime,
'cli': CLIRuntime,
}
# Try to import third-party runtimes if available
_THIRD_PARTY_RUNTIME_CLASSES: dict[str, type[Runtime]] = {}
# Dynamically discover and import third-party runtimes
# Check if third_party package exists and discover runtimes
try:
import third_party.runtime.impl
third_party_base = 'third_party.runtime.impl'
# List of potential third-party runtime modules to try
# These are discovered from the third_party directory structure
potential_runtimes = []
try:
import pkgutil
for importer, modname, ispkg in pkgutil.iter_modules(
third_party.runtime.impl.__path__
):
if ispkg:
potential_runtimes.append(modname)
except Exception:
# If discovery fails, no third-party runtimes will be loaded
potential_runtimes = []
# Try to import each discovered runtime
for runtime_name in potential_runtimes:
try:
module_path = f'{third_party_base}.{runtime_name}.{runtime_name}_runtime'
module = importlib.import_module(module_path)
# Try different class name patterns
possible_class_names = [
f'{runtime_name.upper()}Runtime', # E2BRuntime
f'{runtime_name.capitalize()}Runtime', # E2bRuntime, DaytonaRuntime, etc.
]
runtime_class = None
for class_name in possible_class_names:
try:
runtime_class = getattr(module, class_name)
break
except AttributeError:
continue
if runtime_class:
_THIRD_PARTY_RUNTIME_CLASSES[runtime_name] = runtime_class
except ImportError:
# ImportError means the library is not installed (expected for optional dependencies)
pass
except Exception as e:
# Other exceptions mean the library is present but broken, which should be logged
from openhands.core.logger import openhands_logger as logger
logger.warning(f'Failed to import third-party runtime {module_path}: {e}')
pass
except ImportError:
# third_party package not available
pass
# Combine core and third-party runtimes
_ALL_RUNTIME_CLASSES = {**_DEFAULT_RUNTIME_CLASSES, **_THIRD_PARTY_RUNTIME_CLASSES}
def get_runtime_cls(name: str) -> type[Runtime]:
"""
@@ -93,28 +33,26 @@ def get_runtime_cls(name: str) -> type[Runtime]:
Otherwise attempt to resolve name as subclass of Runtime and return it.
Raise on invalid selections.
"""
if name in _ALL_RUNTIME_CLASSES:
return _ALL_RUNTIME_CLASSES[name]
if name in _DEFAULT_RUNTIME_CLASSES:
return _DEFAULT_RUNTIME_CLASSES[name]
try:
return get_impl(Runtime, name)
except Exception as e:
known_keys = _ALL_RUNTIME_CLASSES.keys()
known_keys = _DEFAULT_RUNTIME_CLASSES.keys()
raise ValueError(
f'Runtime {name} not supported, known are: {known_keys}'
) from e
# Build __all__ list dynamically based on available runtimes
__all__ = [
'Runtime',
'E2BRuntime',
'RemoteRuntime',
'ModalRuntime',
'RunloopRuntime',
'DockerRuntime',
'DaytonaRuntime',
'KubernetesRuntime',
'CLIRuntime',
'LocalRuntime',
'get_runtime_cls',
]
# Add third-party runtimes to __all__ if they're available
for runtime_name, runtime_class in _THIRD_PARTY_RUNTIME_CLASSES.items():
__all__.append(runtime_class.__name__)

View File

@@ -20,7 +20,6 @@ from contextlib import asynccontextmanager
from pathlib import Path
from zipfile import ZipFile
import puremagic
from binaryornot.check import is_binary
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
@@ -52,7 +51,6 @@ from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.observation import (
CmdOutputObservation,
ErrorObservation,
FileDownloadObservation,
FileEditObservation,
FileReadObservation,
FileWriteObservation,
@@ -195,8 +193,6 @@ class ActionExecutor:
self.start_time = time.time()
self.last_execution_time = self.start_time
self._initialized = False
self.downloaded_files: list[str] = []
self.downloads_directory = '/workspace/.downloads'
self.max_memory_gb: int | None = None
if _override_max_memory_gb := os.environ.get('RUNTIME_MAX_MEMORY_GB', None):
@@ -607,45 +603,7 @@ class ActionExecutor:
'Browser functionality is not supported on Windows.'
)
await self._ensure_browser_ready()
browser_observation = await browse(action, self.browser, self.initial_cwd)
if not browser_observation.error:
return browser_observation
else:
curr_files = os.listdir(self.downloads_directory)
new_download = False
for file in curr_files:
if file not in self.downloaded_files:
new_download = True
self.downloaded_files.append(file)
break # FIXME: assuming only one file will be downloaded for simplicity
if not new_download:
return browser_observation
else:
# A new file is downloaded in self.downloads_directory, shift file to /workspace
src_path = os.path.join(
self.downloads_directory, self.downloaded_files[-1]
)
# Guess extension of file using puremagic and add it to tgt_path file name
file_ext = ''
try:
guesses = puremagic.magic_file(src_path)
if len(guesses) > 0:
ext = guesses[0].extension.strip()
if len(ext) > 0:
file_ext = ext
except Exception as _:
pass
tgt_path = os.path.join(
'/workspace', f'file_{len(self.downloaded_files)}{file_ext}'
)
shutil.copy(src_path, tgt_path)
file_download_obs = FileDownloadObservation(
content=f'Execution of the previous action {action.browser_actions} resulted in a file download. The downloaded file is saved at location: {tgt_path}',
file_path=tgt_path,
)
return file_download_obs
return await browse(action, self.browser, self.initial_cwd)
def close(self):
self.memory_monitor.stop_monitoring()

Some files were not shown because too many files have changed in this diff Show More