Compare commits

..

63 Commits

Author SHA1 Message Date
openhands
93287ef9ac Fix microagent test filenames to match expected names
- Change test filenames from 'test.md' to match expected microagent names
- Use 'default.md' for tests expecting 'default' name
- Use 'custom_name.md' for test expecting 'custom_name' name
- Use 'test_agent.md' for test expecting 'test_agent' name
- This properly tests the filename-based naming behavior
2025-06-24 14:20:34 +00:00
openhands
e70595f46f Fix microagent tests and remove debug prints
- Update test assertions to expect filename as microagent name instead of 'default'
- Remove debug print statements from microagent.py
- Revert pytest-asyncio dependency addition as requested
- All tests now pass with the new filename-based naming behavior
2025-06-24 14:16:20 +00:00
openhands
1d3ff66987 Fix failing tests: add missing newlines and pytest-asyncio dependency
- Add missing newlines at end of microagent files (fixed by pre-commit)
- Add pytest-asyncio dependency to fix async test execution
- All non-Docker tests now pass
2025-06-24 14:01:12 +00:00
Xingyao Wang
1a95f86802 fix all remaining issue' 2025-06-23 17:49:02 -04:00
Xingyao Wang
eee12bfd94 fix test 2025-06-23 16:09:32 -04:00
Xingyao Wang
8c2d4dbe8b Merge branch 'main' into update-microagent-docs 2025-06-23 14:22:56 -04:00
Xingyao Wang
0ca3188afa Merge branch 'main' into update-microagent-docs 2025-06-18 14:23:58 -04:00
openhands
283f503870 Exclude name field in MicroagentMetadata as it's deprecated 2025-06-08 22:07:33 +00:00
openhands
0691e5c0d0 Remove type: field from all microagent markdown files 2025-06-08 19:48:01 +00:00
openhands
fc16da8fd2 Update microagent documentation to clarify that type field is optional 2025-06-08 19:39:17 +00:00
openhands
bd3ff43c67 Remove name field from microagent files 2025-06-08 19:35:06 +00:00
openhands
0fe5b808af Update microagent code to use filename as name when not specified 2025-06-08 19:34:59 +00:00
openhands
6c49686ff0 Add MCP tools documentation and update microagent field requirements 2025-06-08 19:30:21 +00:00
openhands
17212bb2f2 Remove unused fields from microagent code and update all microagent files 2025-06-08 19:26:56 +00:00
openhands
9d9f931e95 Remove unused fields from microagent documentation and example 2025-06-08 19:23:47 +00:00
openhands
6fe9680474 Consolidate task microagent documentation into keyword-triggered microagents 2025-06-08 19:19:44 +00:00
Xingyao Wang
53c80d1c92 Merge branch 'main' into update-microagent-docs 2025-06-08 15:17:37 -04:00
openhands
401262f353 Update documentation for task microagents with user input support 2025-06-08 19:15:31 +00:00
Xingyao Wang
58845b01a3 rename more files 2025-06-08 14:30:37 -04:00
Xingyao Wang
469d184157 address engel comment 2025-06-08 14:28:22 -04:00
Xingyao Wang
4837c4dc74 Update microagents/get_test_to_pass.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-09 02:24:23 +08:00
Xingyao Wang
6763f21cc3 Merge branch 'main' into add-back-microagents 2025-06-07 16:47:00 -04:00
Xingyao Wang
32e610ac1d revert unnecessary change 2025-06-07 16:30:55 -04:00
Xingyao Wang
85c65391ca revert changes 2025-06-03 13:53:27 -04:00
Xingyao Wang
c444dbfbbf remove fe changes 2025-06-03 12:04:37 -04:00
Xingyao Wang
dd988d0f14 revert fe 2025-06-03 12:03:00 -04:00
Xingyao Wang
6f1a74e286 merge main 2025-06-03 11:37:51 -04:00
Xingyao Wang
7b956b6103 revert docs to look like main 2025-06-03 11:35:57 -04:00
openhands
34b097115d Fix linting issues in frontend and Python code 2025-05-19 01:39:48 +00:00
openhands
3e4ab4f379 Fix docstring formatting in KnowledgeMicroagent class 2025-05-19 01:29:23 +00:00
openhands
54cd9f7e44 Fix unlocalized strings in microagent-dropdown.tsx 2025-05-19 01:26:33 +00:00
openhands
802b765f98 Add microagent button and dropdown to trajectory actions 2025-05-17 12:05:13 +00:00
openhands
18c88f99ff Merge from main to resolve conflicts 2025-05-17 06:56:11 +00:00
openhands
f3934be07b Fix microagent suggestions using tippy.js for better popup handling 2025-05-12 12:55:00 +00:00
openhands
6ce9f49d1e Fix linting issues in TipTap editor component 2025-05-12 11:06:15 +00:00
openhands
fc07622b20 Implement microagent suggestions using TipTap 2025-05-12 11:00:08 +00:00
Xingyao Wang
da935f9d8f Merge branch 'main' into add-back-microagents 2025-05-03 00:04:17 +08:00
openhands
642cc52a1a Fix linting issues in handlers.ts 2025-05-02 13:06:21 +00:00
openhands
4c361ab9e5 Add mock handler for microagents endpoint 2025-05-02 09:23:25 +00:00
openhands
5dfa1bb6eb Fix microagent suggestions UI and TypeScript errors 2025-05-02 09:21:15 +00:00
Xingyao Wang
a07cf972a5 Merge commit '6032d2620d6ec252d3c80695a6de1fc88da9c87a' into add-back-microagents 2025-05-02 09:03:17 +00:00
openhands
f2e3bc3254 Fix microagent suggestions feature 2025-05-02 08:52:19 +00:00
openhands
3790ec7d60 Add tests for microagent suggestions component 2025-05-02 03:31:41 +00:00
openhands
3c0719309e Add microagent suggestions feature to chat input 2025-05-02 02:57:57 +00:00
Xingyao Wang
0236e0943e fix test 2025-05-02 02:09:27 +00:00
Xingyao Wang
cd464c0022 rename files 2025-05-01 10:38:04 +08:00
Xingyao Wang
4519a7f4f3 fix test 2025-05-01 02:29:52 +00:00
Xingyao Wang
fdc591330b add remain 2025-05-01 02:25:38 +00:00
Xingyao Wang
98e454e82c fix lint and missing imports 2025-05-01 02:25:24 +00:00
Xingyao Wang
e088d2d24a simplify microagent 2025-05-01 02:13:46 +00:00
Xingyao Wang
58c574af1e revert changes 2025-05-01 02:13:00 +00:00
Xingyao Wang
405f0069f8 revert some changes 2025-05-01 02:03:06 +00:00
Xingyao Wang
f26d770d03 remove hardcoded last line 2025-05-01 02:01:51 +00:00
Xingyao Wang
bf2c3de219 cleanup tests 2025-04-30 11:11:23 +08:00
Xingyao Wang
7c35ce16e5 Merge branch 'main' into add-back-microagents 2025-04-30 11:07:17 +08:00
Xingyao Wang
f4024ccd94 Update microagents/update_pr_description.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:43:37 +08:00
Xingyao Wang
b55bfed831 Update microagents/address_pr_comments.md
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-04-30 10:38:22 +08:00
OpenHands Bot
cb0994027f 🤖 Auto-fix Python linting issues 2025-04-29 16:02:04 +00:00
openhands
bcc9bd0b9a Move task microagent tests to test_microagent_task.py 2025-04-29 02:12:14 +00:00
openhands
6c144e6b5a Add back microagent files with special handling for user inputs 2025-04-29 02:06:42 +00:00
openhands
e90b841b0d Update microagent files to match original ones with added triggers and variable prompts 2025-04-29 01:48:10 +00:00
openhands
a1e6ed4dff Add special handling for microagents that require user input 2025-04-29 01:47:18 +00:00
openhands
ad6311d3cd Add back microagent files and add special handling for user input variables 2025-04-29 01:33:23 +00:00
93 changed files with 522 additions and 1122 deletions

View File

@@ -121,7 +121,7 @@ A specialized prompt that enhances OpenHands with domain-specific knowledge, rep
A central repository of available microagents and their configurations.
#### Public Microagent
A general-purpose microagent available to all OpenHands users, triggered by specific keywords. Located in `microagents/`.
A general-purpose microagent available to all OpenHands users, triggered by specific keywords.
#### Repository Microagent
A type of microagent that provides repository-specific context and guidelines, stored in the `.openhands/microagents/` directory.

View File

@@ -68,29 +68,6 @@ If you are starting a pull request (PR), please follow the template in `.github/
These details may or may not be useful for your current task.
### Microagents
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They are Markdown files that can include frontmatter for configuration.
#### Types:
- **Public Microagents**: Located in `microagents/`, available to all users
- **Repository Microagents**: Located in `.openhands/microagents/`, specific to this repository
#### Loading Behavior:
- **Without frontmatter**: Always loaded into LLM context
- **With triggers in frontmatter**: Only loaded when user's message matches the specified trigger keywords
#### Structure:
```yaml
---
triggers:
- keyword1
- keyword2
---
# Microagent Content
Your specialized knowledge and instructions here...
```
### Frontend
#### Action Handling:

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.46-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.45-nikolaik`
## Develop inside Docker container

View File

@@ -62,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **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.
@@ -85,14 +85,15 @@ works best, but you have [many options](https://docs.all-hands.dev/usage/llms).
## 💡 Other ways to run OpenHands
> [!WARNING]
> [!CAUTION]
> OpenHands is meant to be run by a single user on their local workstation.
> It is not appropriate for multi-tenant deployments where multiple users share the same instance. There is no built-in authentication, isolation, or scalability.
>
> If you're interested in running OpenHands in a multi-tenant environment, check out the source-available, commercially-licensed
> [OpenHands Cloud Helm Chart](https://github.com/all-Hands-AI/OpenHands-cloud)
> If you're interested in running OpenHands in a multi-tenant environment, please
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> for advanced deployment options.
You can [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/usage/how-to/headless-mode),
interact with it via a [friendly CLI](https://docs.all-hands.dev/usage/how-to/cli-mode),
or run it on tagged issues with [a github action](https://docs.all-hands.dev/usage/how-to/github-action).

View File

@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。

View File

@@ -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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -201,27 +201,6 @@ model = "gpt-4o"
#native_tool_calling = None
# Safety settings for models that support them (e.g., Mistral AI, Gemini)
# Example for Mistral AI:
# safety_settings = [
# { "category" = "hate", "threshold" = "low" },
# { "category" = "harassment", "threshold" = "low" },
# { "category" = "sexual", "threshold" = "low" },
# { "category" = "dangerous", "threshold" = "low" }
# ]
#
# Example for Gemini:
# safety_settings = [
# { "category" = "HARM_CATEGORY_HARASSMENT", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_HATE_SPEECH", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_SEXUALLY_EXPLICIT", "threshold" = "BLOCK_NONE" },
# { "category" = "HARM_CATEGORY_DANGEROUS_CONTENT", "threshold" = "BLOCK_NONE" }
# ]
#safety_settings = []
[llm.draft_editor]
# The number of times llm_editor tries to fix an error when editing.
correct_num = 5
[llm.gpt4o-mini]
api_key = ""
@@ -339,9 +318,6 @@ classpath = "my_package.my_module.MyCustomAgent"
# Enable GPU support in the runtime
#enable_gpu = false
# When there are multiple cards, you can specify the GPU by ID
#cuda_visible_devices = ''
# Additional Docker runtime kwargs
#docker_runtime_kwargs = {}

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.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.45-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/)
exclude: docs/modules/python
- id: end-of-file-fixer
exclude: ^(docs/|modules/|python/|openhands-ui/)
exclude: docs/modules/python
- id: check-yaml
args: ["--allow-multiple-documents"]
- id: debug-statements

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.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.45-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

@@ -1,13 +1,8 @@
---
title: Configuration Options
description: This page outlines all available configuration options for OpenHands, allowing you to customize its
behavior and integrate it with other services.
description: This page outlines all available configuration options for OpenHands, allowing you to customize its behavior and integrate it with other services. In GUI Mode, any settings applied through the Settings UI will take precedence.
---
<Note>
In GUI Mode, any settings applied through the Settings UI will take precedence.
</Note>
## Core Configuration
The core configuration options are defined in the `[core]` section of the `config.toml` file.

View File

@@ -14,34 +14,34 @@ for scripting.
**Note** - OpenHands requires Python version 3.12 or higher (Python 3.14 is not currently supported)
1. Install OpenHands using pip:
```bash
pip install openhands-ai
```
Or if you prefer not to manage your own Python environment, you can use `uvx`:
Or if you prefer not to manage your own Python environment, you can use `uvx`:
```bash
uvx --python 3.12 --from openhands-ai openhands
```
2. Launch an interactive OpenHands conversation from the command line:
```bash
openhands
```
<Note>
If you have cloned the repository, you can also run the CLI directly using Poetry:
poetry run python -m openhands.cli.main
</Note>
3. Set your model, API key, and other preferences using the UI (or alternatively environment variables, below).
This command opens an interactive prompt where you can type tasks or commands and get responses from OpenHands.
The first time you run the CLI, it will take you through configuring the required LLM
settings. These will be saved for future sessions.
The conversation history will be saved in `~/.openhands/sessions`.
#### For Developers
If you have cloned the repository, you can run the CLI directly using Poetry:
```bash
poetry run python -m openhands.cli.main
```
### Running with Docker
@@ -55,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.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -64,21 +64,16 @@ 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.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
python -m openhands.cli.main --override-cli-mode true
```
<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.
</Note>
> **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.
This launches the CLI in Docker, allowing you to interact with OpenHands.
This launches the CLI in Docker, allowing you to interact with OpenHands as described above.
The `-e SANDBOX_USER_ID=$(id -u)` ensures files created by the agent in your workspace have the correct permissions.
The conversation history will be saved in `~/.openhands/sessions`.
## Interactive CLI Overview
### What is CLI Mode?

View File

@@ -1,7 +1,6 @@
---
title: Custom Sandbox
description: This guide is for users that would like to use their own custom Docker image for the runtime.
For example, with certain tools or programming languages pre-installed.
description: This guide is for users that would like to use their own custom Docker image for the runtime. For example, with certain tools or programming languages pre-installed.
---
The sandbox is where the agent performs its tasks. Instead of running commands directly on your computer

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.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.45 \
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

@@ -73,15 +73,6 @@ We have a few guides for running OpenHands with specific model providers:
- [OpenAI](/usage/llms/openai-llms)
- [OpenRouter](/usage/llms/openrouter)
## Model Customization
LLM providers have specific settings that can be customized to optimize their performance with OpenHands, such as:
- **Custom Tokenizers**: For specialized models, you can add a suitable tokenizer
- **Native Tool Calling**: Toggle native function/tool calling capabilities
For detailed information about model customization, see [LLM Configuration Options](configuration-options#llm-customization).
### API retries and rate limits
LLM providers typically have rate limits, sometimes very low, and may require retries. OpenHands will automatically

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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
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.46
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.45
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None

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.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.45-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.46
docker.all-hands.dev/all-hands-ai/openhands:0.45
```
> **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

@@ -1,7 +1,6 @@
---
title: Model Context Protocol (MCP)
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you
to extend the agent's capabilities with custom tools.
description: This page outlines how to configure and use the Model Context Protocol (MCP) in OpenHands, allowing you to extend the agent's capabilities with custom tools.
---
## Overview

View File

@@ -5,26 +5,111 @@ description: Keyword-triggered microagents provide OpenHands with specific instr
## Usage
These microagents are only loaded when a prompt includes one of the trigger words.
Keyword-triggered microagents are only loaded when a prompt includes one of the trigger words. There are two types of keyword-triggered microagents:
1. **Standard Keyword Microagents**: Triggered by keywords embedded in text
2. **Command-Style Microagents**: Triggered by command-style inputs (e.g., `/fix_test`) that can prompt for user input
Additionally, there's a special type of microagent that's always active:
3. **Repository Microagents**: Always active for a specific repository, providing repository-specific context and tools
## Frontmatter Syntax
Frontmatter is required for keyword-triggered microagents. It must be placed at the top of the file,
above the guidelines.
above the guidelines. Enclose the frontmatter in triple dashes (---).
Enclose the frontmatter in triple dashes (---) and include the following fields:
### Standard Keyword Microagents
For standard keyword microagents, include the following fields:
| Field | Description | Required | Default |
|------------|--------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`knowledge`) | No | Inferred |
| `triggers` | A list of keywords that activate the microagent. | Yes | None |
| `agent` | The agent this microagent applies to. | No | 'CodeActAgent' |
### Command-Style Microagents
## Example
For command-style microagents that require user input, include the following fields:
Keyword-triggered microagent file example located at `.openhands/microagents/yummy.md`:
```
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`task`) | No | Inferred |
| `triggers` | A list of command triggers (e.g., `/fix_test`) | No | `/[name]` |
| `inputs` | A list of input variables the microagent requires | Yes | None |
### Repository Microagents
Repository microagents are always active for a specific repository. They provide repository-specific context and tools.
| Field | Description | Required | Default |
|------------|------------------------------------------------------------|----------|------------------|
| `name` | The name of the microagent | No | Filename |
| `type` | The type of microagent (`repo`) | No | Inferred |
#### Repository Microagent Example
Here's an example of a repository microagent:
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
---
# Repository Guidelines
This repository follows these coding standards:
1. Use PEP 8 for Python code
2. Use ESLint for JavaScript code
3. Write unit tests for all new features
```
This microagent is always active when working with the repository and provides repository-specific guidelines.
### MCP Tools Support
Microagents can also provide additional MCP (Model-Code-Prompt) tools to the agent. This is useful for extending the agent's capabilities with custom tools.
| Field | Description | Required | Default |
|--------------|-----------------------------------------------------------|----------|------------------|
| `mcp_tools` | Configuration for additional MCP tools | No | None |
#### MCP Tools Example
Here's an example of a microagent that provides an additional MCP tool (the `fetch` tool for accessing web content):
```yaml
---
# The type field is optional and will be inferred as 'repo' when no triggers are present
mcp_tools:
stdio_servers:
- name: "fetch"
command: uvx
args:
- mcp-server-fetch
---
```
This microagent is a repository microagent (always active) that adds the `fetch` tool to the agent's capabilities.
Each input in the `inputs` list requires:
| Field | Description | Required |
|---------------|--------------------------------------------------|----------|
| `name` | The name of the input variable | Yes |
| `description` | A description of what the input should contain | Yes |
## Examples
### Standard Keyword Microagent Example
Standard keyword microagent file example located at `.openhands/microagents/yummy.md`:
```yaml
---
# The type field is optional and will be inferred as 'knowledge' when triggers are present
triggers:
- yummyhappy
- happyyummy
@@ -33,4 +118,58 @@ triggers:
The user has said the magic word. Respond with "That was delicious!"
```
[See examples of microagents triggered by keywords in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)
### Command-Style Microagent Example
Command-style microagent file example located at `.openhands/microagents/fix_test.md`:
```yaml
---
# The type field is optional and will be inferred as 'task' when inputs are present
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.
Help me fix these tests to pass by fixing the {{ FUNCTION_TO_FIX }} function in file {{ FILE_FOR_FUNCTION }}.
PLEASE DO NOT modify the tests by yourself -- Let me know if you think some of the tests are incorrect.
```
## Using Command-Style Microagents
Command-style microagents are designed to streamline common development tasks by providing structured templates for specific operations. They are triggered using a command-style format and will prompt the user for any required inputs.
### How to Use
1. Type `/` in the chat input to see available command-style microagents
2. Select a microagent from the dropdown or type its name (e.g., `/fix_test`)
3. The agent will prompt you for any required inputs
4. Provide the requested information
5. The agent will execute the task with your inputs
### Template Variables
In the body of a command-style microagent, you can reference input variables using the `{{ VARIABLE_NAME }}` syntax. These will be replaced with the user-provided values when the microagent is triggered.
### Available Command-Style Microagents
OpenHands includes several built-in command-style microagents:
| Command | Description |
|----------------------|-------------------------------------------------------|
| `/fix_test` | Fix failing tests by modifying a specific function |
| `/update_test` | Update tests for a new implementation |
| `/update_pr` | Update a pull request description |
| `/address_pr_comments` | Address comments on a pull request |
| `/add_repo_instruction` | Add instructions to the repository microagent |
[See examples of microagents in the official OpenHands repository](https://github.com/All-Hands-AI/OpenHands/tree/main/microagents)

View File

@@ -8,7 +8,7 @@ description: Microagents are specialized prompts that enhance OpenHands with dom
Currently OpenHands supports the following types of microagents:
- [General Microagents](./microagents-repo): General guidelines for OpenHands about the repository.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts.
- [Keyword-Triggered Microagents](./microagents-keyword): Guidelines activated by specific keywords in prompts, including command-style microagents that prompt for user inputs.
To customize OpenHands' behavior, create a .openhands/microagents/ directory in the root of your repository and
add `<microagent_name>.md` files inside. For repository-specific guidelines, you can ask OpenHands to analyze your repository and create a comprehensive `repo.md` file (see [General Microagents](./microagents-repo) for details).
@@ -34,7 +34,7 @@ some-repository/
Each microagent file may include frontmatter that provides additional information. In some cases, this frontmatter
is required:
| Microagent Type | Required |
|---------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents` | Yes |
| Microagent Type | Required |
|------------------------------------------------|----------|
| `General Microagents` | No |
| `Keyword-Triggered Microagents (all types)` | Yes |

View File

@@ -3,6 +3,7 @@ title: Daytona Runtime
description: You can use [Daytona](https://www.daytona.io/) as a runtime provider.
---
## Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
2. Click **"Create Key"**.

View File

@@ -3,6 +3,8 @@ title: Docker Runtime
description: This is the default Runtime that's used when you start OpenHands.
---
This is the default Runtime that's used when you start OpenHands.
## Image
The `SANDBOX_RUNTIME_CONTAINER_IMAGE` from nikolaik is a pre-built runtime image
that contains our Runtime server, as well as some basic utilities for Python and NodeJS.

View File

@@ -3,8 +3,7 @@ title: E2B Runtime
description: E2B is an open-source secure cloud environment (sandbox) made for running AI-generated code and agents.
---
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b)
SDK to spawn and control these sandboxes.
[E2B](https://e2b.dev) offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
## Getting started
@@ -19,13 +18,9 @@ SDK to spawn and control these sandboxes.
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
## OpenHands sandbox
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide
[here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers`
directory. and it's called `openhands`.
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide [here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the `containers` directory. and it's called `openhands`.
## Debugging
You can connect to a running E2B sandbox with E2B CLI in your terminal.
- List all running sandboxes (based on your API key)
@@ -39,6 +34,5 @@ You can connect to a running E2B sandbox with E2B CLI in your terminal.
```
## Links
- [E2B Docs](https://e2b.dev/docs)
- [E2B GitHub](https://github.com/e2b-dev/e2b)

View File

@@ -1,8 +1,6 @@
---
title: Local Runtime
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without
using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios
where Docker is not available.
description: The Local Runtime allows the OpenHands agent to execute actions directly on your local machine without using Docker. This runtime is primarily intended for controlled environments like CI pipelines or testing scenarios where Docker is not available.
---
<Warning>

View File

@@ -1,11 +1,7 @@
---
title: Remote Runtime
description: This runtime is specifically designed for agent evaluation purposes only through the
[OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be
used to launch production OpenHands applications.
description: This runtime is specifically designed for agent evaluation purposes only through the [OpenHands evaluation harness](https://github.com/All-Hands-AI/OpenHands/tree/main/evaluation). It should not be used to launch production OpenHands applications.
---
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details),
it allows you to launch runtimes in parallel in the cloud. Fill out
[this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to
apply if you want to try this out!
OpenHands Remote Runtime is currently in beta (read [here](https://runtime.all-hands.dev/) for more details), it allows you to launch runtimes
in parallel in the cloud. Fill out [this form](https://docs.google.com/forms/d/e/1FAIpQLSckVz_JFwg2_mOxNZjCtr7aoBFI2Mwdan3f75J_TrdMS1JV2g/viewform) to apply if you want to try this out!

View File

@@ -1,7 +1,6 @@
---
title: Runloop Runtime
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the
[runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
---
## Access

View File

@@ -1,6 +1,6 @@
---
title: Search Engine Setup
description: Configure OpenHands to use Tavily as a search engine.
description: Configure OpenHands to use Tavily as a search engine
---
## Setting Up Search Engine in OpenHands
@@ -11,10 +11,10 @@ OpenHands can be configured to use [Tavily](https://tavily.com/) as a search eng
To use the search functionality in OpenHands, you'll need to obtain a Tavily API key:
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account.
2. Navigate to the API section in your dashboard.
3. Generate a new API key.
4. Copy the API key (it should start with `tvly-`).
1. Visit [Tavily's website](https://tavily.com/) and sign up for an account
2. Navigate to the API section in your dashboard
3. Generate a new API key
4. Copy the API key (it should start with `tvly-`)
### Configuring Search in OpenHands
@@ -22,12 +22,13 @@ Once you have your Tavily API key, you can configure OpenHands to use it:
#### In the OpenHands UI
1. Open OpenHands and navigate to the Settings page.
2. Under the `LLM` tab, enter your Tavily API key (starting with `tvly-`) in the `Search API Key (Tavily)` field.
3. Click `Save` to apply the changes.
1. Open OpenHands and navigate to the Settings page by clicking the gear icon
2. In the LLM settings tab, locate the "Search API Key (Tavily)" field
3. Enter your Tavily API key (starting with `tvly-`)
4. Click "Save" to apply the changes
<Note>
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
The search API key field is optional. If you don't provide a key, the search functionality will not be available to the agent.
</Note>
#### Using Configuration Files
@@ -44,23 +45,22 @@ search_api_key = "tvly-your-api-key-here"
When the search engine is configured:
- The agent can decide to search the web when it needs external information.
- Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which
includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
- Results are returned and incorporated into the agent's context.
- The agent can use this information to provide more accurate and up-to-date responses.
1. The agent can decide to search the web when it needs external information
2. Search queries are sent to Tavily's API via [Tavily's MCP server](https://github.com/tavily-ai/tavily-mcp) which includes a variety of [tools](https://docs.tavily.com/documentation/api-reference/introduction) (search, extract, crawl, map).
3. Results are returned and incorporated into the agent's context
4. The agent can use this information to provide more accurate and up-to-date responses
### Limitations
- Search results depend on Tavily's coverage and freshness.
- Usage may be subject to Tavily's rate limits and pricing tiers.
- The agent will only search when it determines that external information is needed.
- Search results depend on Tavily's coverage and freshness
- Usage may be subject to Tavily's rate limits and pricing tiers
- The agent will only search when it determines that external information is needed
### Troubleshooting
If you encounter issues with the search functionality:
- Verify that your API key is correct and active.
- Check that your API key starts with `tvly-`.
- Ensure you have an active internet connection.
- Check Tavily's status page for any service disruptions.
- Verify that your API key is correct and active
- Check that your API key starts with `tvly-`
- Ensure you have an active internet connection
- Check Tavily's status page for any service disruptions

View File

@@ -1,194 +0,0 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { screen } from "@testing-library/react";
import { renderWithProviders } from "test-utils";
import { EventMessage } from "#/components/features/chat/event-message";
vi.mock("#/hooks/query/use-config", () => ({
useConfig: () => ({
data: { APP_MODE: "saas" },
}),
}));
vi.mock("#/hooks/query/use-feedback-exists", () => ({
useFeedbackExists: (eventId: number | undefined) => ({
data: { exists: false },
isLoading: false,
}),
}));
describe("EventMessage", () => {
afterEach(() => {
vi.clearAllMocks();
});
it("should render LikertScale for finish action when it's the last message", () => {
const finishEvent = {
id: 123,
source: "agent" as const,
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
message: "Task completed successfully",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={finishEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should render LikertScale for assistant message when it's the last message", () => {
const assistantMessageEvent = {
id: 456,
source: "agent" as const,
action: "message" as const,
args: {
thought: "I need more information to proceed.",
image_urls: null,
file_urls: [],
wait_for_response: true,
},
message: "I need more information to proceed.",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={assistantMessageEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should render LikertScale for error observation when it's the last message", () => {
const errorEvent = {
id: 789,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-123",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={true}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should NOT render LikertScale when not the last message", () => {
const finishEvent = {
id: 101,
source: "agent" as const,
action: "finish" as const,
args: {
final_thought: "Task completed successfully",
task_completed: "success" as const,
outputs: {},
thought: "Task completed successfully",
},
message: "Task completed successfully",
timestamp: new Date().toISOString(),
};
renderWithProviders(
<EventMessage
event={finishEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={false}
/>
);
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
});
it("should render LikertScale for error observation when in last 10 actions but not last message", () => {
const errorEvent = {
id: 999,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-456",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={true}
/>
);
expect(screen.getByLabelText("Rate 1 stars")).toBeInTheDocument();
expect(screen.getByLabelText("Rate 5 stars")).toBeInTheDocument();
});
it("should NOT render LikertScale for error observation when not in last 10 actions", () => {
const errorEvent = {
id: 888,
source: "user" as const,
observation: "error" as const,
content: "An error occurred",
extras: {
error_id: "test-error-789",
},
message: "An error occurred",
timestamp: new Date().toISOString(),
cause: 123,
};
renderWithProviders(
<EventMessage
event={errorEvent}
hasObservationPair={false}
isAwaitingUserConfirmation={false}
isLastMessage={false}
isInLast10Actions={false}
/>
);
expect(screen.queryByLabelText("Rate 1 stars")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Rate 5 stars")).not.toBeInTheDocument();
});
});

View File

@@ -1,54 +0,0 @@
import { describe, it, expect } from "vitest";
import { extractSettings } from "#/utils/settings-utils";
describe("Model name case preservation", () => {
it("should preserve the original case of model names in extractSettings", () => {
// Create FormData with proper casing
const formData = new FormData();
formData.set("llm-provider-input", "SambaNova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
// Test that model names maintain their original casing
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
it("should preserve openai model case", () => {
const formData = new FormData();
formData.set("llm-provider-input", "openai");
formData.set("llm-model-input", "gpt-4o");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("openai/gpt-4o");
});
it("should preserve anthropic model case", () => {
const formData = new FormData();
formData.set("llm-provider-input", "anthropic");
formData.set("llm-model-input", "claude-sonnet-4-20250514");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
expect(settings.LLM_MODEL).toBe("anthropic/claude-sonnet-4-20250514");
});
it("should not automatically lowercase model names", () => {
const formData = new FormData();
formData.set("llm-provider-input", "SambaNova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("agent", "CodeActAgent");
formData.set("language", "en");
const settings = extractSettings(formData);
// Test that camelCase and PascalCase are preserved
expect(settings.LLM_MODEL).not.toBe("sambanova/meta-llama-3.1-8b-instruct");
expect(settings.LLM_MODEL).toBe("SambaNova/Meta-Llama-3.1-8B-Instruct");
});
});

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.45.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.45.0",
"dependencies": {
"@heroui/react": "^2.8.0-beta.9",
"@microlink/react-json-view": "^1.26.2",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.45.0",
"private": true,
"type": "module",
"engines": {

View File

@@ -35,7 +35,6 @@ interface EventMessageProps {
hasObservationPair: boolean;
isAwaitingUserConfirmation: boolean;
isLastMessage: boolean;
isInLast10Actions: boolean;
}
export function EventMessage({
@@ -43,52 +42,24 @@ export function EventMessage({
hasObservationPair,
isAwaitingUserConfirmation,
isLastMessage,
isInLast10Actions,
}: EventMessageProps) {
const shouldShowConfirmationButtons =
isLastMessage && event.source === "agent" && isAwaitingUserConfirmation;
const { data: config } = useConfig();
// Use our query hook to check if feedback exists and get rating/reason
const {
data: feedbackData = { exists: false },
isLoading: isCheckingFeedback,
} = useFeedbackExists(event.id);
const renderLikertScale = () => {
if (config?.APP_MODE !== "saas" || isCheckingFeedback) {
return null;
}
// For error observations, show if in last 10 actions
// For other events, show only if it's the last message
const shouldShow = isErrorObservation(event)
? isInLast10Actions
: isLastMessage;
if (!shouldShow) {
return null;
}
return (
<LikertScale
eventId={event.id}
initiallySubmitted={feedbackData.exists}
initialRating={feedbackData.rating}
initialReason={feedbackData.reason}
/>
);
};
} = useFeedbackExists(isFinishAction(event) ? event.id : undefined);
if (isErrorObservation(event)) {
return (
<>
<ErrorMessage
errorId={event.extras.error_id}
defaultMessage={event.message}
/>
{renderLikertScale()}
</>
<ErrorMessage
errorId={event.extras.error_id}
defaultMessage={event.message}
/>
);
}
@@ -99,11 +70,24 @@ export function EventMessage({
return null;
}
const showLikertScale =
config?.APP_MODE === "saas" &&
isFinishAction(event) &&
isLastMessage &&
!isCheckingFeedback;
if (isFinishAction(event)) {
return (
<>
<ChatMessage type="agent" message={getEventContent(event).details} />
{renderLikertScale()}
{showLikertScale && (
<LikertScale
eventId={event.id}
initiallySubmitted={feedbackData.exists}
initialRating={feedbackData.rating}
initialReason={feedbackData.reason}
/>
)}
</>
);
}
@@ -112,20 +96,15 @@ export function EventMessage({
const message = parseMessageFromEvent(event);
return (
<>
<ChatMessage type={event.source} message={message}>
{event.args.image_urls && event.args.image_urls.length > 0 && (
<ImageCarousel size="small" images={event.args.image_urls} />
)}
{event.args.file_urls && event.args.file_urls.length > 0 && (
<FileList files={event.args.file_urls} />
)}
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</ChatMessage>
{isAssistantMessage(event) &&
event.action === "message" &&
renderLikertScale()}
</>
<ChatMessage type={event.source} message={message}>
{event.args.image_urls && event.args.image_urls.length > 0 && (
<ImageCarousel size="small" images={event.args.image_urls} />
)}
{event.args.file_urls && event.args.file_urls.length > 0 && (
<FileList files={event.args.file_urls} />
)}
{shouldShowConfirmationButtons && <ConfirmationButtons />}
</ChatMessage>
);
}

View File

@@ -39,7 +39,6 @@ export const Messages: React.FC<MessagesProps> = React.memo(
hasObservationPair={actionHasObservationPair(message)}
isAwaitingUserConfirmation={isAwaitingUserConfirmation}
isLastMessage={messages.length - 1 === index}
isInLast10Actions={messages.length - 1 - index < 10}
/>
))}

View File

@@ -833,10 +833,10 @@
},
"HOME$LETS_START_BUILDING": {
"en": "Let's Start Building!",
"ja": "開発を始めましょう!",
"zh-CN": "让我们开始开发",
"zh-TW": "讓我們開始開發",
"ko-KR": "개발을 시작합시다!",
"ja": "構築を始めましょう!",
"zh-CN": "让我们开始构建",
"zh-TW": "讓我們開始構建",
"ko-KR": "구축을 시작합시다!",
"no": "La oss begynne å bygge!",
"it": "Iniziamo a costruire!",
"pt": "Vamos começar a construir!",
@@ -849,7 +849,7 @@
},
"HOME$OPENHANDS_DESCRIPTION": {
"en": "OpenHands makes it easy to build and maintain software using AI-driven development.",
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの開発と維持を容易にします。",
"ja": "OpenHandsはAI駆動の開発を使用してソフトウェアの構築と維持を容易にします。",
"zh-CN": "OpenHands使用AI驱动的开发方式轻松构建和维护软件。",
"zh-TW": "OpenHands使用AI驅動的開發方式輕鬆構建和維護軟件。",
"ko-KR": "OpenHands는 AI 기반 개발을 사용하여 소프트웨어를 쉽게 구축하고 유지할 수 있게 합니다.",

View File

@@ -111,7 +111,6 @@ const openHandsHandlers = [
"gpt-4o-mini",
"anthropic/claude-3.5",
"anthropic/claude-sonnet-4-20250514",
"sambanova/Meta-Llama-3.1-8B-Instruct",
]),
),

View File

@@ -23,7 +23,6 @@ import { isCustomModel } from "#/utils/is-custom-model";
import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton";
import { KeyStatusIcon } from "#/components/features/settings/key-status-icon";
import { DEFAULT_SETTINGS } from "#/services/settings";
import { getProviderId } from "#/utils/map-provider";
function LlmSettingsScreen() {
const { t } = useTranslation();
@@ -94,15 +93,13 @@ function LlmSettingsScreen() {
};
const basicFormAction = (formData: FormData) => {
const providerDisplay = formData.get("llm-provider-input")?.toString();
const provider = providerDisplay
? getProviderId(providerDisplay)
: undefined;
const provider = formData.get("llm-provider-input")?.toString();
const model = formData.get("llm-model-input")?.toString();
const apiKey = formData.get("llm-api-key-input")?.toString();
const searchApiKey = formData.get("search-api-key-input")?.toString();
const fullLlmModel = provider && model && `${provider}/${model}`;
const fullLlmModel =
provider && model && `${provider}/${model}`.toLowerCase();
saveSettings(
{

View File

@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { parseMaxBudgetPerTask, extractSettings } from "../settings-utils";
import { parseMaxBudgetPerTask } from "../settings-utils";
describe("parseMaxBudgetPerTask", () => {
it("should return null for empty string", () => {
@@ -47,45 +47,3 @@ describe("parseMaxBudgetPerTask", () => {
expect(parseMaxBudgetPerTask("5e-1")).toBeNull(); // 0.5, which is < 1
});
});
describe("extractSettings", () => {
it("should preserve model name case when extracting settings", () => {
// Test cases with various model name formats
const testCases = [
{ provider: "sambanova", model: "Meta-Llama-3.1-8B-Instruct" },
{ provider: "openai", model: "GPT-4o" },
{ provider: "anthropic", model: "Claude-3-5-Sonnet" },
{ provider: "openrouter", model: "CamelCaseModel" },
];
testCases.forEach(({ provider, model }) => {
const formData = new FormData();
formData.set("llm-provider-input", provider);
formData.set("llm-model-input", model);
const settings = extractSettings(formData);
// Verify that the model name case is preserved
const expectedModel = `${provider}/${model}`;
expect(settings.LLM_MODEL).toBe(expectedModel);
// Only test that it's not lowercased if the original has uppercase letters
if (expectedModel !== expectedModel.toLowerCase()) {
expect(settings.LLM_MODEL).not.toBe(expectedModel.toLowerCase());
}
});
});
it("should handle custom model without lowercasing", () => {
const formData = new FormData();
formData.set("llm-provider-input", "sambanova");
formData.set("llm-model-input", "Meta-Llama-3.1-8B-Instruct");
formData.set("use-advanced-options", "true");
formData.set("custom-model", "Custom-Model-Name");
const settings = extractSettings(formData);
// Custom model should take precedence and preserve case
expect(settings.LLM_MODEL).toBe("Custom-Model-Name");
expect(settings.LLM_MODEL).not.toBe("custom-model-name");
});
});

View File

@@ -29,10 +29,3 @@ export const mapProvider = (provider: string) =>
Object.keys(MAP_PROVIDER).includes(provider)
? MAP_PROVIDER[provider as keyof typeof MAP_PROVIDER]
: provider;
export const getProviderId = (displayName: string): string => {
const entry = Object.entries(MAP_PROVIDER).find(
([, value]) => value === displayName,
);
return entry ? entry[0] : displayName;
};

View File

@@ -1,12 +1,10 @@
import { Settings } from "#/types/settings";
import { getProviderId } from "#/utils/map-provider";
const extractBasicFormData = (formData: FormData) => {
const providerDisplay = formData.get("llm-provider-input")?.toString();
const provider = providerDisplay ? getProviderId(providerDisplay) : undefined;
const provider = formData.get("llm-provider-input")?.toString();
const model = formData.get("llm-model-input")?.toString();
const LLM_MODEL = `${provider}/${model}`;
const LLM_MODEL = `${provider}/${model}`.toLowerCase();
const LLM_API_KEY = formData.get("llm-api-key-input")?.toString();
const AGENT = formData.get("agent")?.toString();
const LANGUAGE = formData.get("language")?.toString();

View File

@@ -1,20 +1,16 @@
---
name: add_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
- new agent
- new microagent
- create agent
- create an agent
- create microagent
- create a microagent
- add agent
- add an agent
- add microagent
- add a microagent
- microagent template
---
This agent helps create new microagents in the `.openhands/microagents` directory by providing guidance and templates.

View File

@@ -1,13 +1,9 @@
---
name: add_repo_inst
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: REPO_FOLDER_NAME
triggers:
- /add_repo_inst
inputs:
- name: REPO_FOLDER_NAME
description: "Branch for the agent to work on"
---
Please browse the current repository under /workspace/{{ REPO_FOLDER_NAME }}, look at the documentation and relevant code, and understand the purpose of this repository.
@@ -18,7 +14,6 @@ Here's an example:
```markdown
---
name: repo
type: repo
agent: CodeActAgent
---

View File

@@ -1,15 +1,11 @@
---
name: address_pr_comments
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: URL of the pull request
name: PR_URL
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
triggers:
- /address_pr_comments
inputs:
- name: PR_URL
description: "URL of the pull request"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
---
First, check the branch {{ BRANCH_NAME }} and read the diff against the main branch to understand the purpose.

View File

@@ -1,8 +1,4 @@
---
name: agent_memory
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- /remember
---

View File

@@ -1,15 +1,8 @@
---
# This is a repo microagent that is always activated
# to include necessary default tools implemented with MCP
name: default-tools
type: repo
version: 1.0.0
agent: CodeActAgent
mcp_tools:
stdio_servers:
- name: "fetch"
command: "uvx"
args: ["mcp-server-fetch"]
# We leave the body empty because MCP tools will automatically add the
# tool description for LLMs in tool calls, so there's no need to add extra descriptions.
- args:
- mcp-server-fetch
command: uvx
name: fetch
---

View File

@@ -1,8 +1,4 @@
---
name: docker
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- docker
- container

View File

@@ -1,19 +1,16 @@
---
name: fix_test
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
- description: The name of function to fix
name: FUNCTION_TO_FIX
- description: The path of the file that contains the function
name: FILE_FOR_FUNCTION
triggers:
- /fix_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
- name: FUNCTION_TO_FIX
description: "The name of function to fix"
- name: FILE_FOR_FUNCTION
description: "The path of the file that contains the function"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.

View File

@@ -1,8 +1,4 @@
---
name: flarglebargle
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- flarglebargle
---

View File

@@ -1,8 +1,4 @@
---
name: github
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- github
- git

View File

@@ -1,8 +1,4 @@
---
name: gitlab
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- gitlab
- git

View File

@@ -1,8 +1,4 @@
---
name: kubernetes
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- kubernetes
- k8s

View File

@@ -1,8 +1,4 @@
---
name: npm
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- npm
---

View File

@@ -1,8 +1,4 @@
---
name: pdflatex
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- pdflatex
---

View File

@@ -1,15 +1,12 @@
---
name: security
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- security
- vulnerability
- authentication
- authorization
- permissions
- security
- vulnerability
- authentication
- authorization
- permissions
---
This document provides guidance on security best practices
You should always be considering security implications when developing.

View File

@@ -1,16 +1,12 @@
---
name: SSH Microagent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
- ssh
- remote server
- remote machine
- remote host
- remote connection
- secure shell
- ssh keys
---
# SSH Microagent

View File

@@ -1,12 +1,8 @@
---
name: swift-linux
type: knowledge
agent: CodeActAgent
version: 1.0.0
triggers:
- swift-linux
- swift-debian
- swift-installation
triggers:
- swift-linux
- swift-debian
- swift-installation
---
# Swift Installation Guide for Debian Linux

View File

@@ -1,19 +1,15 @@
---
name: update_pr_description
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: URL of the pull request
name: PR_URL
type: string
validation:
pattern: ^https://github.com/.+/.+/pull/[0-9]+$
- description: Branch name corresponds to the pull request
name: BRANCH_NAME
type: string
triggers:
- /update_pr_description
inputs:
- name: PR_URL
description: "URL of the pull request"
type: string
validation:
pattern: "^https://github.com/.+/.+/pull/[0-9]+$"
- name: BRANCH_NAME
description: "Branch name corresponds to the pull request"
type: string
---
Please check the branch "{{ BRANCH_NAME }}" and look at the diff against the main branch. This branch belongs to this PR "{{ PR_URL }}".

View File

@@ -1,15 +1,12 @@
---
name: update_test
version: 1.0.0
author: openhands
agent: CodeActAgent
inputs:
- description: Branch for the agent to work on
name: BRANCH_NAME
- description: The test command you want the agent to work on. For example, `pytest
tests/unit/test_bash_parsing.py`
name: TEST_COMMAND_TO_RUN
triggers:
- /update_test
inputs:
- name: BRANCH_NAME
description: "Branch for the agent to work on"
- name: TEST_COMMAND_TO_RUN
description: "The test command you want the agent to work on. For example, `pytest tests/unit/test_bash_parsing.py`"
---
Can you check out branch "{{ BRANCH_NAME }}", and run {{ TEST_COMMAND_TO_RUN }}.

View File

@@ -1,34 +0,0 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -1,15 +0,0 @@
# openhands-ui
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.16. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -1,25 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "openhands-ui",
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.2.16", "", { "dependencies": { "bun-types": "1.2.16" } }, "sha512-1aCZJ/6nSiViw339RsaNhkNoEloLaPzZhxMOYEa7OzRzO41IGg5n/7I43/ZIAW/c+Q6cT12Vf7fOZOoVIzb5BQ=="],
"@types/node": ["@types/node@24.0.3", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg=="],
"bun-types": ["bun-types@1.2.16", "", { "dependencies": { "@types/node": "*" } }, "sha512-ciXLrHV4PXax9vHvUrkvun9VPVGOVwbbbBF/Ev1cXz12lyEZMoJpIJABOfPcN9gDJRaiKF9MVbSygLg4NXu3/A=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
}
}

View File

@@ -1 +0,0 @@
console.log("Hello via Bun!");

View File

@@ -1,17 +0,0 @@
{
"name": "@openhands/ui",
"version": "0.1.0",
"description": "OpenHands UI Components",
"keywords": [
"openhands",
"ui",
"components"
],
"type": "module",
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5.8.3"
}
}

View File

@@ -1,29 +0,0 @@
{
"compilerOptions": {
// Environment setup & latest features
"lib": ["ESNext"],
"target": "ESNext",
"module": "Preserve",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,
// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}

View File

@@ -1,6 +1,6 @@
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
from openhands.core.config.condenser_config import CondenserConfig, NoOpCondenserConfig
from openhands.core.config.extended_config import ExtendedConfig
@@ -47,7 +47,7 @@ class AgentConfig(BaseModel):
extended: ExtendedConfig = Field(default_factory=lambda: ExtendedConfig({}))
"""Extended configuration for the agent."""
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, AgentConfig]:

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import Literal, cast
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
from openhands.core import logger
from openhands.core.config.llm_config import LLMConfig
@@ -13,7 +13,7 @@ class NoOpCondenserConfig(BaseModel):
type: Literal['noop'] = Field('noop')
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class ObservationMaskingCondenserConfig(BaseModel):
@@ -26,7 +26,7 @@ class ObservationMaskingCondenserConfig(BaseModel):
ge=1,
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class BrowserOutputCondenserConfig(BaseModel):
@@ -55,7 +55,7 @@ class RecentEventsCondenserConfig(BaseModel):
default=100, description='Maximum number of events to keep.', ge=1
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class LLMSummarizingCondenserConfig(BaseModel):
@@ -82,7 +82,7 @@ class LLMSummarizingCondenserConfig(BaseModel):
description='Maximum length of the event representations to be passed to the LLM.',
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class AmortizedForgettingCondenserConfig(BaseModel):
@@ -102,7 +102,7 @@ class AmortizedForgettingCondenserConfig(BaseModel):
ge=0,
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class LLMAttentionCondenserConfig(BaseModel):
@@ -125,7 +125,7 @@ class LLMAttentionCondenserConfig(BaseModel):
ge=0,
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class StructuredSummaryCondenserConfig(BaseModel):
@@ -152,7 +152,7 @@ class StructuredSummaryCondenserConfig(BaseModel):
description='Maximum length of the event representations to be passed to the LLM.',
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class CondenserPipelineConfig(BaseModel):
@@ -167,7 +167,7 @@ class CondenserPipelineConfig(BaseModel):
description='List of condenser configurations to be used in the pipeline.',
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
# Type alias for convenience

View File

@@ -51,7 +51,7 @@ def get_field_info(field: FieldInfo) -> dict[str, Any]:
def model_defaults_to_dict(model: BaseModel) -> dict[str, Any]:
"""Serialize field information in a dict for the frontend, including type hints, defaults, and whether it's optional."""
result = {}
for name, field in model.__class__.model_fields.items():
for name, field in model.model_fields.items():
field_value = getattr(model, name)
if isinstance(field_value, BaseModel):

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
class KubernetesConfig(BaseModel):
@@ -62,7 +62,7 @@ class KubernetesConfig(BaseModel):
description='Run the runtime sandbox container in privileged mode for use with docker-in-docker',
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, 'KubernetesConfig']:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import os
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, SecretStr, ValidationError
from pydantic import BaseModel, Field, SecretStr, ValidationError
from openhands.core.logger import LOG_DIR
from openhands.core.logger import openhands_logger as logger
@@ -45,7 +45,6 @@ class LLMConfig(BaseModel):
native_tool_calling: Whether to use native tool calling if supported by the model. Can be True, False, or not set.
reasoning_effort: The effort to put into reasoning. This is a string that can be one of 'low', 'medium', 'high', or 'none'. Exclusive for o1 models.
seed: The seed to use for the LLM.
safety_settings: Safety settings for models that support them (like Mistral AI and Gemini).
"""
model: str = Field(default='claude-sonnet-4-20250514')
@@ -87,12 +86,8 @@ class LLMConfig(BaseModel):
native_tool_calling: bool | None = Field(default=None)
reasoning_effort: str | None = Field(default='high')
seed: int | None = Field(default=None)
safety_settings: list[dict[str, str]] | None = Field(
default=None,
description='Safety settings for models that support them (like Mistral AI and Gemini)',
)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, LLMConfig]:

View File

@@ -2,7 +2,7 @@ import os
from typing import TYPE_CHECKING
from urllib.parse import urlparse
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from pydantic import BaseModel, Field, ValidationError, model_validator
if TYPE_CHECKING:
from openhands.core.config.openhands_config import OpenHandsConfig
@@ -72,7 +72,7 @@ class MCPConfig(BaseModel):
stdio_servers: list[MCPStdioServerConfig] = Field(default_factory=list)
shttp_servers: list[MCPSHTTPServerConfig] = Field(default_factory=list)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@staticmethod
def _normalize_servers(servers_data: list[dict | str]) -> list[dict]:

View File

@@ -1,7 +1,7 @@
import os
from typing import Any, ClassVar
from pydantic import BaseModel, ConfigDict, Field, SecretStr
from pydantic import BaseModel, Field, SecretStr
from openhands.core import logger
from openhands.core.config.agent_config import AgentConfig
@@ -114,7 +114,7 @@ class OpenHandsConfig(BaseModel):
defaults_dict: ClassVar[dict] = {}
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
def get_llm_config(self, name: str = 'llm') -> LLMConfig:
"""'llm' is the name for default config (for backward compatibility prior to 0.8)."""

View File

@@ -1,6 +1,6 @@
import os
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
from pydantic import BaseModel, Field, ValidationError, model_validator
class SandboxConfig(BaseModel):
@@ -88,8 +88,7 @@ class SandboxConfig(BaseModel):
description="Volume mounts in the format 'host_path:container_path[:mode]', e.g. '/my/host/dir:/workspace:rw'. Multiple mounts can be specified using commas, e.g. '/path1:/workspace/path1,/path2:/workspace/path2:ro'",
)
cuda_visible_devices: str | None = Field(default=None)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, 'SandboxConfig']:

View File

@@ -1,4 +1,4 @@
from pydantic import BaseModel, ConfigDict, Field, ValidationError
from pydantic import BaseModel, Field, ValidationError
class SecurityConfig(BaseModel):
@@ -12,7 +12,7 @@ class SecurityConfig(BaseModel):
confirmation_mode: bool = Field(default=False)
security_analyzer: str | None = Field(default=None)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, 'SecurityConfig']:

View File

@@ -67,7 +67,7 @@ def load_from_env(
# helper function to set attributes based on env vars
def set_attr_from_env(sub_config: BaseModel, prefix: str = '') -> None:
"""Set attributes of a config model based on environment variables."""
for field_name, field_info in sub_config.__class__.model_fields.items():
for field_name, field_info in sub_config.model_fields.items():
field_value = getattr(sub_config, field_name)
field_type = field_info.annotation

View File

@@ -5,7 +5,6 @@ from typing import Annotated, Any, Coroutine, Literal, overload
from pydantic import (
BaseModel,
ConfigDict,
Field,
SecretStr,
WithJsonSchema,
@@ -35,10 +34,10 @@ class ProviderToken(BaseModel):
user_id: str | None = Field(default=None)
host: str | None = Field(default=None)
model_config = ConfigDict(
frozen=True, # Makes the entire model immutable
validate_assignment=True,
)
model_config = {
'frozen': True, # Makes the entire model immutable
'validate_assignment': True,
}
@classmethod
def from_value(cls, token_value: ProviderToken | dict[str, str]) -> ProviderToken:
@@ -63,10 +62,10 @@ class CustomSecret(BaseModel):
secret: SecretStr = Field(default_factory=lambda: SecretStr(''))
description: str = Field(default='')
model_config = ConfigDict(
frozen=True, # Makes the entire model immutable
validate_assignment=True,
)
model_config = {
'frozen': True, # Makes the entire model immutable
'validate_assignment': True,
}
@classmethod
def from_value(cls, secret_value: CustomSecret | dict[str, str]) -> CustomSecret:

View File

@@ -182,12 +182,6 @@ class LLM(RetryMixin, DebugMixin):
kwargs['max_tokens'] = self.config.max_output_tokens
kwargs.pop('max_completion_tokens')
# Add safety settings for models that support them
if 'mistral' in self.config.model.lower() and self.config.safety_settings:
kwargs['safety_settings'] = self.config.safety_settings
elif 'gemini' in self.config.model.lower() and self.config.safety_settings:
kwargs['safety_settings'] = self.config.safety_settings
self._completion = partial(
litellm_completion,
model=self.config.model,

View File

@@ -4,7 +4,7 @@ from fastmcp import Client
from fastmcp.client.transports import SSETransport, StreamableHttpTransport
from mcp import McpError
from mcp.types import CallToolResult
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
from openhands.core.config.mcp_config import MCPSHTTPServerConfig, MCPSSEServerConfig
from openhands.core.logger import openhands_logger as logger
@@ -16,13 +16,14 @@ class MCPClient(BaseModel):
A collection of tools that connects to an MCP server and manages available tools through the Model Context Protocol.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
client: Optional[Client] = None
description: str = 'MCP client tools for server interaction'
tools: list[MCPClientTool] = Field(default_factory=list)
tool_map: dict[str, MCPClientTool] = Field(default_factory=dict)
class Config:
arbitrary_types_allowed = True
async def _initialize_and_list_tools(self) -> None:
"""Initialize session and populate tool map."""
if not self.client:

View File

@@ -1,5 +1,4 @@
from mcp.types import Tool
from pydantic import ConfigDict
class MCPClientTool(Tool):
@@ -10,7 +9,8 @@ class MCPClientTool(Tool):
by the MCPClient for each operation.
"""
model_config = ConfigDict(arbitrary_types_allowed=True)
class Config:
arbitrary_types_allowed = True
def to_param(self) -> dict:
"""Convert tool to function call format."""

View File

@@ -40,6 +40,11 @@ class BaseMicroagent(BaseModel):
derived_name = None
if microagent_dir is not None:
derived_name = str(path.relative_to(microagent_dir).with_suffix(''))
else:
derived_name = path.with_suffix('').name
logger.warning(
f'No microagent_dir provided. Microagent name will be the file name: {derived_name}'
)
# Only load directly from path if file_content is not provided
if file_content is None:
@@ -95,6 +100,16 @@ class BaseMicroagent(BaseModel):
MicroagentType.TASK: TaskMicroagent,
}
# We will always use derived_name if available
assert derived_name is not None
agent_name = derived_name
if metadata.name is not None:
logger.warning(
f'Detected `name:` field in frontmatter for microagent {metadata.name}. '
"This is deprecated. Microagent's name will use the file name "
f'({derived_name}) instead.'
)
# Infer the agent type:
# 1. If inputs exist -> TASK
# 2. If triggers exist -> KNOWLEDGE
@@ -102,8 +117,7 @@ class BaseMicroagent(BaseModel):
inferred_type: MicroagentType
if metadata.inputs:
inferred_type = MicroagentType.TASK
# Add a trigger for the agent name if not already present
trigger = f'/{metadata.name}'
trigger = f'/{agent_name}'
if not metadata.triggers or trigger not in metadata.triggers:
if not metadata.triggers:
metadata.triggers = [trigger]
@@ -120,9 +134,6 @@ class BaseMicroagent(BaseModel):
# This should theoretically not happen with the logic above
raise ValueError(f'Could not determine microagent type for: {path}')
# Use derived_name if available (from relative path), otherwise fallback to metadata.name
agent_name = derived_name if derived_name is not None else metadata.name
agent_class = subclass_map[inferred_type]
return agent_class(
name=agent_name,

View File

@@ -25,10 +25,12 @@ class InputMetadata(BaseModel):
class MicroagentMetadata(BaseModel):
"""Metadata for all microagents."""
name: str = 'default'
name: str = Field(default='default', exclude=True)
type: MicroagentType = Field(default=MicroagentType.REPO_KNOWLEDGE)
version: str = Field(default='1.0.0')
agent: str = Field(default='CodeActAgent')
# Keep these fields for backward compatibility but they're not used
version: str = Field(default='1.0.0', exclude=True)
agent: str = Field(default='CodeActAgent', exclude=True)
author: str = Field(default='', exclude=True)
triggers: list[str] = [] # optional, only exists for knowledge microagents
inputs: list[InputMetadata] = [] # optional, only exists for task microagents
mcp_tools: MCPConfig | None = (

View File

@@ -720,8 +720,7 @@ fi
)
# Clean up the org repo directory
action = CmdRunAction(f'rm -rf {org_repo_dir}')
self.run_action(action)
shutil.rmtree(org_repo_dir)
else:
self.log(
'info',

View File

@@ -360,21 +360,7 @@ class DockerRuntime(ActionExecutionClient):
)
command = self.get_action_execution_server_startup_command()
if self.config.sandbox.enable_gpu:
gpu_ids = self.config.sandbox.cuda_visible_devices
if gpu_ids is None:
device_requests = [
docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)
]
else:
device_requests = [
docker.types.DeviceRequest(
capabilities=[['gpu']],
device_ids=[str(i) for i in gpu_ids.split(',')],
)
]
else:
device_requests = None
try:
if self.runtime_container_image is None:
raise ValueError('Runtime container image is not set')
@@ -390,7 +376,11 @@ class DockerRuntime(ActionExecutionClient):
detach=True,
environment=environment,
volumes=volumes, # type: ignore
device_requests=device_requests,
device_requests=(
[docker.types.DeviceRequest(capabilities=[['gpu']], count=-1)]
if self.config.sandbox.enable_gpu
else None
),
**(self.config.sandbox.docker_runtime_kwargs or {}),
)
self.log('debug', f'Container started. Server url: {self.api_url}')

View File

@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
```toml
[sandbox]
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik"
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.45-nikolaik"
```
#### Additional Kubernetes Options

View File

@@ -39,40 +39,6 @@ Within the `<updated_code>` tag, include only the final code after updation. Do
<update_snippet>{draft_changes}</update_snippet>
"""
CORRECT_SYS_MSG = """You are a code repair assistant. Now you have an original file content and error information from a static code checking tool (lint tool). Your task is to automatically modify and return the repaired complete code based on these error messages and refer to the current file content.
The following are the specific task steps you need to complete:
Carefully read the current file content to ensure that you fully understand its code structure.
According to the lint error prompt, accurately locate and analyze the cause of the problem.
Modify the original file content and fix all errors prompted by the lint tool.
Return complete, runnable, and error-fixed code, paying attention to maintaining the overall style and specifications of the original code.
Please note:
Please strictly follow the lint error prompts to make modifications and do not miss any problems.
The modified code must be complete and cannot introduce new errors or bugs.
The modified code must maintain the original code function and logic, and no changes unrelated to error repair should be made."""
CORRECT_USER_MSG = """
THE FOLLOWING ARE THE ORIGINAL FILE CONTENTS AND THE ERROR INFORMATION REPORTED BY THE LINT TOOL
# CURRENT FILE CONTENT:
```
{file_content}
```
# ERROR MESSAGE FROM STATIC CODE CHECKING TOOL:
```
{lint_error}
```
""".strip()
def _extract_code(string: str) -> str | None:
pattern = r'<updated_code>(.*?)</updated_code>'
@@ -230,7 +196,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
return ErrorObservation(error_message)
return None
def llm_based_edit(self, action: FileEditAction, retry_num: int = 0) -> Observation:
def llm_based_edit(self, action: FileEditAction) -> Observation:
obs = self.read(FileReadAction(path=action.path))
if (
isinstance(obs, ErrorObservation)
@@ -287,14 +253,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
diff,
)
if error_obs is not None:
self.write(
FileWriteAction(path=action.path, content=updated_content)
)
return self.correct_edit(
file_content=updated_content,
error_obs=error_obs,
retry_num=retry_num,
)
return error_obs
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
return FileEditObservation(
@@ -321,8 +280,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
error_msg = (
f'[Edit error: The range of lines to edit is too long.]\n'
f'[The maximum number of lines allowed to edit at once is {self.MAX_LINES_TO_EDIT}. '
f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n'
# [start_idx, end_idx), so no need to + 1
f'Got (L{start_idx + 1}-L{end_idx}) {length_of_range} lines.]\n' # [start_idx, end_idx), so no need to + 1
)
# search for relevant ranges to hint the agent
topk_chunks: list[Chunk] = get_top_k_chunk_matches(
@@ -375,12 +333,7 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
)
if error_obs is not None:
error_obs.llm_metrics = self.draft_editor_llm.metrics
self.write(FileWriteAction(path=action.path, content=updated_content))
return self.correct_edit(
file_content=updated_content,
error_obs=error_obs,
retry_num=retry_num,
)
return error_obs
obs = self.write(FileWriteAction(path=action.path, content=updated_content))
ret_obs = FileEditObservation(
@@ -392,40 +345,3 @@ class FileEditRuntimeMixin(FileEditRuntimeInterface):
)
ret_obs.llm_metrics = self.draft_editor_llm.metrics
return ret_obs
def check_retry_num(self, retry_num):
correct_num = self.draft_editor_llm.config.correct_num
return correct_num < retry_num
def correct_edit(
self, file_content: str, error_obs: ErrorObservation, retry_num: int = 0
) -> Observation:
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
from openhands.agenthub.codeact_agent.tools import LLMBasedFileEditTool
from openhands.llm.llm_utils import check_tools
_retry_num = retry_num + 1
if self.check_retry_num(_retry_num):
return error_obs
tools = check_tools([LLMBasedFileEditTool], self.draft_editor_llm.config)
messages = [
{'role': 'system', 'content': CORRECT_SYS_MSG},
{
'role': 'user',
'content': CORRECT_USER_MSG.format(
file_content=file_content, lint_error=error_obs.content
),
},
]
params: dict = {'messages': messages, 'tools': tools}
try:
response = self.draft_editor_llm.completion(**params)
actions = codeact_function_calling.response_to_actions(response)
if len(actions) != 1:
return error_obs
for action in actions:
if isinstance(action, FileEditAction):
return self.llm_based_edit(action, _retry_num)
except Exception as e:
logger.error(f'correct lint error is failed: {e}')
return error_obs

View File

@@ -7,7 +7,7 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, status
from fastapi.responses import JSONResponse
from jinja2 import Environment, FileSystemLoader
from pydantic import BaseModel, ConfigDict, Field
from pydantic import BaseModel, Field
from openhands.core.config.llm_config import LLMConfig
from openhands.core.logger import openhands_logger as logger
@@ -88,7 +88,7 @@ class InitSessionRequest(BaseModel):
if os.getenv('ALLOW_SET_CONVERSATION_ID', '0') == '1':
conversation_id: str = Field(default_factory=lambda: uuid.uuid4().hex)
model_config = ConfigDict(extra='forbid')
model_config = {'extra': 'forbid'}
class ConversationResponse(BaseModel):

View File

@@ -1,4 +1,4 @@
from pydantic import ConfigDict, Field
from pydantic import Field
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE
from openhands.integrations.service_types import ProviderType
@@ -18,6 +18,6 @@ class ConversationInitData(Settings):
conversation_instructions: str | None = Field(default=None)
git_provider: ProviderType | None = Field(default=None)
model_config = ConfigDict(
arbitrary_types_allowed=True,
)
model_config = {
'arbitrary_types_allowed': True,
}

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from pydantic import (
BaseModel,
ConfigDict,
SecretStr,
)
@@ -40,7 +39,7 @@ class GETSettingsModel(Settings):
llm_api_key_set: bool
search_api_key_set: bool = False
model_config = ConfigDict(use_enum_values=True)
model_config = {'use_enum_values': True}
class CustomSecretWithoutValueModel(BaseModel):

View File

@@ -1,12 +1,5 @@
import os
from datetime import datetime, timezone
from openhands.core.config.utils import load_openhands_config
from openhands.core.logger import openhands_logger as logger
from openhands.server.config.server_config import ServerConfig
from openhands.storage.conversation.conversation_store import ConversationStore
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
from openhands.utils.conversation_summary import get_default_conversation_title
from openhands.utils.import_utils import get_impl
@@ -30,44 +23,7 @@ class ConversationValidator:
cookies_str: str,
authorization_header: str | None = None,
) -> str | None:
user_id = None
metadata = await self._ensure_metadata_exists(conversation_id, user_id)
return metadata.user_id
async def _ensure_metadata_exists(
self,
conversation_id: str,
user_id: str | None,
) -> ConversationMetadata:
config = load_openhands_config()
server_config = ServerConfig()
conversation_store_class: type[ConversationStore] = get_impl(
ConversationStore,
server_config.conversation_store_class,
)
conversation_store = await conversation_store_class.get_instance(
config, user_id
)
try:
metadata = await conversation_store.get_metadata(conversation_id)
except FileNotFoundError:
logger.info(
f'Creating new conversation metadata for {conversation_id}',
extra={'session_id': conversation_id},
)
await conversation_store.save_metadata(
ConversationMetadata(
conversation_id=conversation_id,
user_id=user_id,
title=get_default_conversation_title(conversation_id),
last_updated_at=datetime.now(timezone.utc),
selected_repository=None,
)
)
metadata = await conversation_store.get_metadata(conversation_id)
return metadata
return None
def create_conversation_validator() -> ConversationValidator:

View File

@@ -2,7 +2,6 @@ from __future__ import annotations
from pydantic import (
BaseModel,
ConfigDict,
Field,
SecretStr,
SerializationInfo,
@@ -46,9 +45,9 @@ class Settings(BaseModel):
email: str | None = None
email_verified: bool | None = None
model_config = ConfigDict(
validate_assignment=True,
)
model_config = {
'validate_assignment': True,
}
@field_serializer('llm_api_key', 'search_api_key')
def api_key_serializer(self, api_key: SecretStr | None, info: SerializationInfo):

View File

@@ -3,7 +3,6 @@ from typing import Any
from pydantic import (
BaseModel,
ConfigDict,
Field,
SerializationInfo,
field_serializer,
@@ -32,11 +31,11 @@ class UserSecrets(BaseModel):
default_factory=lambda: MappingProxyType({})
)
model_config = ConfigDict(
frozen=True,
validate_assignment=True,
arbitrary_types_allowed=True,
)
model_config = {
'frozen': True,
'validate_assignment': True,
'arbitrary_types_allowed': True,
}
@field_serializer('provider_tokens')
def provider_tokens_serializer(

19
poetry.lock generated
View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aioboto3"
@@ -462,7 +462,7 @@ description = "LTS Port of Python audioop"
optional = false
python-versions = ">=3.13"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"},
{file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"},
@@ -1644,7 +1644,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@@ -3053,8 +3053,8 @@ files = [
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev"
proto-plus = [
{version = ">=1.22.3,<2.0.0dev"},
{version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0dev"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev"
@@ -3076,8 +3076,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0"
grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""}
proto-plus = [
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
]
protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
requests = ">=2.18.0,<3.0.0"
@@ -3295,8 +3295,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpc-google-iam-v1 = ">=0.14.0,<1.0.0"
proto-plus = [
{version = ">=1.22.3,<2.0.0"},
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
@@ -6586,8 +6586,8 @@ files = [
[package.dependencies]
googleapis-common-protos = ">=1.52,<2.0"
grpcio = [
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
{version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""},
]
opentelemetry-api = ">=1.15,<2.0"
opentelemetry-exporter-otlp-proto-common = "1.34.1"
@@ -9350,7 +9350,6 @@ files = [
{file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"},
{file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"},
]
markers = {evaluation = "platform_system == \"Linux\" and platform_machine == \"x86_64\""}
[package.extras]
check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
@@ -9593,7 +9592,7 @@ description = "Standard library aifc redistribution. \"dead battery\"."
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"},
{file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"},
@@ -9610,7 +9609,7 @@ description = "Standard library chunk redistribution. \"dead battery\"."
optional = false
python-versions = "*"
groups = ["main"]
markers = "python_version >= \"3.13\""
markers = "python_version == \"3.13\""
files = [
{file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"},
{file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"},

View File

@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.46.0"
version = "0.45.0"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"

View File

@@ -1,7 +1,6 @@
"""Tests for microagent loading in runtime."""
import os
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
@@ -20,7 +19,7 @@ from openhands.microagent.microagent import (
RepoMicroagent,
TaskMicroagent,
)
from openhands.microagent.types import MicroagentType
from openhands.microagent.types import InputMetadata, MicroagentType
def _create_test_microagents(test_dir: str):
@@ -32,10 +31,6 @@ def _create_test_microagents(test_dir: str):
knowledge_dir = microagents_dir / 'knowledge'
knowledge_dir.mkdir(exist_ok=True)
knowledge_agent = """---
name: test_knowledge_agent
type: knowledge
version: 1.0.0
agent: CodeActAgent
triggers:
- test
- pytest
@@ -45,17 +40,10 @@ triggers:
Testing best practices and guidelines.
"""
(knowledge_dir / 'knowledge.md').write_text(knowledge_agent)
(knowledge_dir / 'test_knowledge_agent.md').write_text(knowledge_agent)
# Create test repo agent
repo_agent = """---
name: test_repo_agent
type: repo
version: 1.0.0
agent: CodeActAgent
---
# Test Repository Agent
repo_agent = """# Test Repository Agent
Repository-specific test instructions.
"""
@@ -89,7 +77,7 @@ def test_load_microagents_with_trailing_slashes(
# Check knowledge agents
assert len(knowledge_agents) == 1
agent = knowledge_agents[0]
assert agent.name == 'knowledge/knowledge'
assert agent.name == 'knowledge/test_knowledge_agent'
assert 'test' in agent.triggers
assert 'pytest' in agent.triggers
@@ -126,7 +114,7 @@ def test_load_microagents_with_selected_repo(temp_dir, runtime_cls, run_as_openh
# Check knowledge agents
assert len(knowledge_agents) == 1
agent = knowledge_agents[0]
assert agent.name == 'knowledge/knowledge'
assert agent.name == 'knowledge/test_knowledge_agent'
assert 'test' in agent.triggers
assert 'pytest' in agent.triggers
@@ -180,7 +168,7 @@ Repository-specific test instructions.
_close_test_runtime(runtime)
def test_task_microagent_creation():
def test_task_microagent_creation(temp_dir):
"""Test that a TaskMicroagent is created correctly."""
content = """---
name: test_task
@@ -196,21 +184,43 @@ inputs:
This is a test task microagent with a variable: ${test_var}.
"""
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(content)
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.type == MicroagentType.TASK
assert agent.name == 'test_task'
assert '/test_task' in agent.triggers
assert "If the user didn't provide any of these variables" in agent.content
assert agent.inputs == [InputMetadata(name='TEST_VAR', description='Test variable')]
simplified_content = """---
triggers:
- /test_task
inputs:
- name: TEST_VAR
description: "Test variable"
---
assert isinstance(agent, TaskMicroagent)
assert agent.type == MicroagentType.TASK
assert agent.name == 'test_task'
assert '/test_task' in agent.triggers
assert "If the user didn't provide any of these variables" in agent.content
This is a test task microagent with a variable: ${test_var}.
"""
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(simplified_content)
simplified_agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
assert isinstance(simplified_agent, TaskMicroagent)
assert simplified_agent.type == MicroagentType.TASK
assert simplified_agent.name == 'test_task'
assert '/test_task' in simplified_agent.triggers
assert (
"If the user didn't provide any of these variables" in simplified_agent.content
)
def test_task_microagent_variable_extraction():
def test_task_microagent_variable_extraction(temp_dir):
"""Test that variables are correctly extracted from the content."""
content = """---
name: test_task
@@ -227,19 +237,18 @@ inputs:
This is a test with variables: ${var1}, ${var2}, and ${var3}.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(content)
agent = BaseMicroagent.load(f.name)
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
assert isinstance(agent, TaskMicroagent)
variables = agent.extract_variables(agent.content)
assert set(variables) == {'var1', 'var2', 'var3'}
assert agent.requires_user_input()
assert isinstance(agent, TaskMicroagent)
variables = agent.extract_variables(agent.content)
assert set(variables) == {'var1', 'var2', 'var3'}
assert agent.requires_user_input()
def test_knowledge_microagent_no_prompt():
def test_knowledge_microagent_no_prompt(temp_dir):
"""Test that a regular KnowledgeMicroagent doesn't get the prompt."""
content = """---
name: test_knowledge
@@ -252,19 +261,17 @@ triggers:
This is a test knowledge microagent.
"""
with open(os.path.join(temp_dir, 'test_knowledge.md'), 'w') as f:
f.write(content)
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_knowledge.md'))
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, KnowledgeMicroagent)
assert agent.type == MicroagentType.KNOWLEDGE
assert "If the user didn't provide any of these variables" not in agent.content
assert isinstance(agent, KnowledgeMicroagent)
assert agent.type == MicroagentType.KNOWLEDGE
assert "If the user didn't provide any of these variables" not in agent.content
def test_task_microagent_trigger_addition():
def test_task_microagent_trigger_addition(temp_dir):
"""Test that a trigger is added if not present."""
content = """---
name: test_task
@@ -278,18 +285,16 @@ inputs:
This is a test task microagent.
"""
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(content)
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert '/test_task' in agent.triggers
assert isinstance(agent, TaskMicroagent)
assert '/test_task' in agent.triggers
def test_task_microagent_no_duplicate_trigger():
def test_task_microagent_no_duplicate_trigger(temp_dir):
"""Test that a trigger is not duplicated if already present."""
content = """---
name: test_task
@@ -306,21 +311,19 @@ inputs:
This is a test task microagent.
"""
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(content)
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
agent = BaseMicroagent.load(f.name)
assert isinstance(agent, TaskMicroagent)
assert agent.triggers.count('/test_task') == 1 # No duplicates
assert len(agent.triggers) == 2
assert 'another_trigger' in agent.triggers
assert '/test_task' in agent.triggers
assert isinstance(agent, TaskMicroagent)
assert agent.triggers.count('/test_task') == 1 # No duplicates
assert len(agent.triggers) == 2
assert 'another_trigger' in agent.triggers
assert '/test_task' in agent.triggers
def test_task_microagent_match_trigger():
def test_task_microagent_match_trigger(temp_dir):
"""Test that a task microagent matches its trigger correctly."""
content = """---
name: test_task
@@ -337,17 +340,16 @@ inputs:
This is a test task microagent.
"""
with tempfile.NamedTemporaryFile(suffix='.md') as f:
f.write(content.encode())
f.flush()
with open(os.path.join(temp_dir, 'test_task.md'), 'w') as f:
f.write(content)
agent = BaseMicroagent.load(f.name)
agent = BaseMicroagent.load(os.path.join(temp_dir, 'test_task.md'))
assert isinstance(agent, TaskMicroagent)
assert agent.match_trigger('/test_task') == '/test_task'
assert agent.match_trigger(' /test_task ') == '/test_task'
assert agent.match_trigger('This contains /test_task') == '/test_task'
assert agent.match_trigger('/other_task') is None
assert isinstance(agent, TaskMicroagent)
assert agent.match_trigger('/test_task') == '/test_task'
assert agent.match_trigger(' /test_task ') == '/test_task'
assert agent.match_trigger('This contains /test_task') == '/test_task'
assert agent.match_trigger('/other_task') is None
def test_default_tools_microagent_exists():
@@ -369,15 +371,12 @@ def test_default_tools_microagent_exists():
with open(default_tools_path, 'r') as f:
content = f.read()
# Verify it's a repo microagent (always activated)
assert 'type: repo' in content, 'default-tools.md should be a repo microagent'
assert 'command: uvx' in content, 'default-tools.md should use uvx command'
assert 'mcp-server-fetch' in content, 'default-tools.md should use mcp-server-fetch'
# Verify it has the fetch tool configured
assert 'name: "fetch"' in content, 'default-tools.md should have a fetch tool'
assert 'command: "uvx"' in content, 'default-tools.md should use uvx command'
assert 'args: ["mcp-server-fetch"]' in content, (
'default-tools.md should use mcp-server-fetch'
)
agent = BaseMicroagent.load(default_tools_path)
assert isinstance(agent, RepoMicroagent)
@pytest.mark.asyncio

View File

@@ -7,7 +7,7 @@ from openhands.microagent.types import MicroagentType
def test_load_markdown_without_frontmatter():
"""Test loading a markdown file without frontmatter."""
content = '# Test Content\nThis is a test markdown file without frontmatter.'
path = Path('test.md')
path = Path('default.md')
# Load the agent from content using keyword argument
agent = BaseMicroagent.load(path=path, file_content=content)
@@ -26,7 +26,7 @@ def test_load_markdown_with_empty_frontmatter():
content = (
'---\n---\n# Test Content\nThis is a test markdown file with empty frontmatter.'
)
path = Path('test.md')
path = Path('default.md')
# Load the agent from content using keyword argument
agent = BaseMicroagent.load(path=path, file_content=content)
@@ -50,12 +50,12 @@ name: custom_name
---
# Test Content
This is a test markdown file with partial frontmatter."""
path = Path('test.md')
path = Path('custom_name.md')
# Load the agent from content using keyword argument
agent = BaseMicroagent.load(path=path, file_content=content)
# Verify it uses provided name but default values for other fields
# Verify it uses filename instead of provided name (filename takes precedence)
assert isinstance(agent, RepoMicroagent)
assert agent.name == 'custom_name'
assert (
@@ -77,12 +77,12 @@ version: 2.0.0
---
# Test Content
This is a test markdown file with full frontmatter."""
path = Path('test.md')
path = Path('test_agent.md')
# Load the agent from content using keyword argument
agent = BaseMicroagent.load(path=path, file_content=content)
# Verify all provided values are used
# Verify filename is used for name but other metadata values are preserved
assert isinstance(agent, RepoMicroagent)
assert agent.name == 'test_agent'
assert (