Compare commits

..

45 Commits

Author SHA1 Message Date
openhands
68ca0b6a9b Add evaluation changes without disabling repository memory 2025-04-15 15:06:58 +00:00
Shotaro Sano
e0fcd7a61e Fix issue #6098: Prevent duplicate error message display in chat interface (#7858) 2025-04-15 16:21:23 +04:00
Ryan H. Tran
e9989d1085 Upgrade openhands-aci to 0.2.10 (#7810) 2025-04-15 18:43:44 +07:00
Xingyao Wang
49c515b252 frontend: Display think action as action rather than text (#7852)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-15 09:09:16 +08:00
Robert Brennan
2d05578c21 Fix links in readme (#7854) 2025-04-15 02:27:25 +04:00
Engel Nyst
d05a6f30e1 [Refactor] Rename codeact_* agent options to simple name (#7853) 2025-04-15 00:14:13 +02:00
Calvin Smith
10c81c39fb Fix export conversation button in Safari (#7662)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
2025-04-14 15:10:20 -06:00
sumeetkumar1701
2d599349ef fix:Transmitting accurate head parameter in cross-repository pull requests. (#7788) 2025-04-14 17:57:15 +00:00
mamoodi
33caf5c6ca Update feature template to add note about adding reaction (#7847) 2025-04-14 13:56:04 -04:00
Ciocanel Razvan
a9850766a7 Allow input for pr_type openhands-resolver.yml (#7619)
Co-authored-by: Rohit Malhotra <rohitvinodmalhotra@gmail.com>
2025-04-14 17:53:58 +00:00
OpenHands
77e2416def Fix issue #7826: [Bug]: Chat input box is too small (#7827) 2025-04-14 12:19:38 -05:00
蔡政特
02af9865ec fix: Runtime local docker environment HTTPStatusError (#7648)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-04-14 15:41:54 +00:00
dependabot[bot]
75ca2aa6b1 chore(deps): bump the version-all group with 10 updates (#7846)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-14 17:16:27 +02:00
mamoodi
d820661592 Update docs on why we use sandbox user (#7845) 2025-04-14 11:01:35 -04:00
Robert Brennan
1ff351a4f1 Add OpenHands Cloud to README, other minor tweaks (#7844) 2025-04-14 14:01:52 +00:00
OpenHands
78b8e58561 Fix issue #7837: [Bug]: Unit tests for tool use support (#7838)
Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
2025-04-14 15:45:37 +02:00
tofarr
fddbfce51a Fix for race condition in cache (#7812) 2025-04-12 07:43:34 -06:00
Rohit Malhotra
20d3766451 [Fix]: Use better auth header for GitLab microagent (#7828) 2025-04-11 20:09:28 -04:00
sp.wack
72b5e18898 fix(backend): Return 400 if trying to open a binary file (#7825) 2025-04-11 22:47:57 +00:00
Rohit Malhotra
03b8b8c19a (Chore): Rm single provider legacy code (#7821)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-11 18:21:35 +00:00
Panduka Muditha
7c2f1b075e feat: CLI enhancements to support /init, /help and /exit (#7801)
Co-authored-by: Bashwara Undupitiya <bashwarau@verdentra.com>
2025-04-11 14:13:41 -04:00
Graham Neubig
883da1b28c Add extensive typing to openhands/runtime/plugins directory (#7726)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-12 02:02:53 +08:00
Engel Nyst
bb98d94b35 [evaluation] fix missing metadata (#7819) 2025-04-11 16:58:59 +00:00
sp.wack
d114c45135 chore: Improve pre-commit (#7818) 2025-04-11 20:55:26 +04:00
Calvin Smith
36e092e0ac fix: Disable prompt caching in default condenser (#7781)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Calvin Smith <calvin@all-hands.dev>
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-04-11 10:09:23 -06:00
Ray Myers
e2bb69908a chore - Rebuild docker image in fork CI instead of using artifacts (#7809) 2025-04-11 11:06:46 -05:00
Ray Myers
cd33c5eac7 Revert "chore - User docker cache mount for vscode server archive (#7… (#7817) 2025-04-11 16:04:50 +00:00
dependabot[bot]
0f8a139fb5 chore(deps): bump the version-all group with 5 updates (#7814)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-11 17:06:59 +02:00
Xingyao Wang
ced4ee3038 Fix: Display accumulated token usage in frontend metrics (#7803)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-11 22:38:47 +08:00
Robert Brennan
cc19dac939 move typecheck (#7804) 2025-04-10 17:37:14 -04:00
Ray Myers
4c7c73a6f2 chore - User docker cache mount for vscode server archive (#7785) 2025-04-10 16:31:20 -05:00
Robert Brennan
0493fea9fc fix for status messages not showing up (#7802) 2025-04-10 17:16:33 -04:00
Robert Brennan
1c2db9f468 Fix chat background on mobile devices (#7798)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-04-10 15:40:43 -04:00
Ray Myers
c210230802 chore - Add more workers to speed up runtime CI tests (#7796) 2025-04-10 14:10:29 -05:00
mamoodi
9b12989a36 Update OpenHands cloud docs with conversation persistence section (#7794)
Co-authored-by: Robert Brennan <accounts@rbren.io>
2025-04-10 14:10:20 -04:00
dependabot[bot]
f1bcd72cd8 chore(deps): bump the version-all group with 8 updates (#7792)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-10 19:22:16 +02:00
Ray Myers
a5c57fbd3a chore - Do not install playwright in CI build steps (#7786) 2025-04-10 11:55:02 -05:00
NarwhalChen
513f7ab7e7 fix(llm): ensure base_url has protocol prefix for model info fetch when using LiteLLM (#7782)
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
2025-04-10 20:10:06 +04:00
juanmichelini
53c0c5a07b SWE-bench_verified instruction baseline improvements to 60% (#7546)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-04-10 16:08:27 +00:00
Robert Brennan
d924e7cea5 fix occurrences of MicroAgent to the standard "Microagent" (#7791) 2025-04-10 15:23:19 +00:00
mamoodi
7910a90522 Release 0.32.0 (#7761) 2025-04-10 08:45:04 -04:00
Duc Pham
35d49f6941 feat (backend): Add support for MCP servers natively via CodeActAgent (#7637)
Co-authored-by: trungbach <trunga2k29@gmail.com>
Co-authored-by: quangdz1704 <Ntq.1704@gmail.com>
Co-authored-by: Xingyao Wang <xingyao6@illinois.edu>
2025-04-10 01:59:13 +00:00
dependabot[bot]
e359a4affa chore(deps-dev): bump typescript from 5.8.2 to 5.8.3 in /docs in the version-all group (#7779)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ray Myers <ray.myers@gmail.com>
2025-04-10 01:21:13 +04:00
Robert Brennan
159f79f9d8 fix i18n script (#7783) 2025-04-09 20:45:11 +00:00
dependabot[bot]
827c19ccd9 chore(deps): bump the version-all group with 5 updates (#7777)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-09 18:29:28 +00:00
144 changed files with 3256 additions and 999 deletions

View File

@@ -12,3 +12,6 @@ assignees: ''
**Describe the UX or technical implementation you have in mind**
**Additional context**
### If you find this feature request or enhancement useful, make sure to add a 👍 to the issue

View File

@@ -145,7 +145,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Create source distribution and Dockerfile
run: poetry run python3 openhands/runtime/utils/runtime_build.py --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
- name: Lowercase Repository Owner
@@ -174,20 +174,19 @@ jobs:
build-args: ${{ env.DOCKER_BUILD_ARGS }}
context: containers/runtime
provenance: false
# Forked repos can't push to GHCR, so we need to upload the image as an artifact
# Forked repos can't push to GHCR, so we just build in order to populate the cache for rebuilding
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
outputs: type=docker,dest=/tmp/runtime-${{ matrix.base_image.tag }}.tar
context: containers/runtime
- name: Upload runtime image for fork
- name: Upload runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/upload-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp/runtime-${{ matrix.base_image.tag }}.tar
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
verify_hash_equivalence_in_runtime_and_app:
name: Verify Hash Equivalence in Runtime and Docker images
@@ -220,7 +219,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
run: make install-python-dependencies POETRY_GROUP=main INSTALL_PLAYWRIGHT=0
- name: Get hash in App Image
run: |
echo "Hash from app image: ${{ needs.ghcr_build_app.outputs.hash_from_app_image }}"
@@ -248,7 +247,7 @@ jobs:
test_runtime_root:
name: RT Unit Tests (Root)
needs: [ghcr_build_runtime, define-matrix]
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: blacksmith-8vcpu-ubuntu-2204
strategy:
fail-fast: false
matrix:
@@ -258,17 +257,23 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp
- name: Load runtime image for fork
if: github.event.pull_request.head.repo.fork
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
@@ -286,10 +291,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Run docker runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs
@@ -308,7 +310,7 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=false \
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:
@@ -317,7 +319,7 @@ jobs:
# Run unit tests with the Docker runtime Docker images as openhands user
test_runtime_oh:
name: RT Unit Tests (openhands)
runs-on: blacksmith-4vcpu-ubuntu-2204
runs-on: blacksmith-8vcpu-ubuntu-2204
needs: [ghcr_build_runtime, define-matrix]
strategy:
matrix:
@@ -327,17 +329,23 @@ jobs:
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
# Forked repos can't push to GHCR, so we need to download the image as an artifact
- name: Download runtime image for fork
- name: Download runtime source for fork
if: github.event.pull_request.head.repo.fork
uses: actions/download-artifact@v4
with:
name: runtime-${{ matrix.base_image.tag }}
path: /tmp
- name: Load runtime image for fork
if: github.event.pull_request.head.repo.fork
name: runtime-src-${{ matrix.base_image.tag }}
path: containers/runtime
- name: Lowercase Repository Owner
run: |
docker load --input /tmp/runtime-${{ matrix.base_image.tag }}.tar
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
# Forked repos can't push to GHCR, so we need to rebuild using cache
- name: Build runtime image ${{ matrix.base_image.image }} for fork
if: github.event.pull_request.head.repo.fork
uses: useblacksmith/build-push-action@v1
with:
load: true
tags: ghcr.io/${{ env.REPO_OWNER }}/runtime:${{ env.RELEVANT_SHA }}-${{ matrix.base_image.tag }}
context: containers/runtime
- name: Cache Poetry dependencies
uses: useblacksmith/cache@v5
with:
@@ -355,10 +363,7 @@ jobs:
- name: Install poetry via pipx
run: pipx install poetry
- name: Install Python dependencies using Poetry
run: make install-python-dependencies
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
run: make install-python-dependencies POETRY_GROUP=main,test,runtime INSTALL_PLAYWRIGHT=0
- name: Run runtime tests
run: |
# We install pytest-xdist in order to run tests across CPUs
@@ -374,7 +379,7 @@ jobs:
SANDBOX_RUNTIME_CONTAINER_IMAGE=$image_name \
TEST_IN_CI=true \
RUN_AS_OPENHANDS=true \
poetry run pytest -n 3 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py
poetry run pytest -n 7 -raRs --reruns 2 --reruns-delay 5 --cov=openhands --cov-report=xml -s ./tests/runtime --ignore=tests/runtime/test_browsergym_envs.py --durations=10
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
env:

View File

@@ -16,6 +16,11 @@ on:
type: string
default: "main"
description: "Target branch to pull and create PR against"
pr_type:
required: false
type: string
default: "draft"
description: "The PR type that is going to be created (draft, ready)"
LLM_MODEL:
required: false
type: string
@@ -280,9 +285,9 @@ jobs:
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \
--target-branch ${{ env.TARGET_BRANCH }} \
--pr-type draft \
--pr-type ${{ inputs.pr_type || 'draft' }} \
--reviewer ${{ github.actor }} | tee pr_result.txt && \
grep "draft created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
grep "PR created" pr_result.txt | sed 's/.*\///g' > pr_number.txt
else
cd /tmp && python -m openhands.resolver.send_pull_request \
--issue-number ${{ env.ISSUE_NUMBER }} \

View File

@@ -133,20 +133,29 @@ install-python-dependencies:
export HNSWLIB_NO_NATIVE=1; \
poetry run pip install chroma-hnswlib; \
fi
@poetry install
@if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
poetry run playwright install chromium; \
@if [ -n "${POETRY_GROUP}" ]; then \
echo "Installing only POETRY_GROUP=${POETRY_GROUP}"; \
poetry install --only $${POETRY_GROUP}; \
else \
if [ ! -f cache/playwright_chromium_is_installed.txt ]; then \
echo "Running playwright install --with-deps chromium..."; \
poetry run playwright install --with-deps chromium; \
mkdir -p cache; \
touch cache/playwright_chromium_is_installed.txt; \
poetry install; \
fi
@if [ "${INSTALL_PLAYWRIGHT}" != "false" ] && [ "${INSTALL_PLAYWRIGHT}" != "0" ]; then \
if [ -f "/etc/manjaro-release" ]; then \
echo "$(BLUE)Detected Manjaro Linux. Installing Playwright dependencies...$(RESET)"; \
poetry run pip install playwright; \
poetry run playwright install chromium; \
else \
echo "Setup already done. Skipping playwright installation."; \
if [ ! -f cache/playwright_chromium_is_installed.txt ]; then \
echo "Running playwright install --with-deps chromium..."; \
poetry run playwright install --with-deps chromium; \
mkdir -p cache; \
touch cache/playwright_chromium_is_installed.txt; \
else \
echo "Setup already done. Skipping playwright installation."; \
fi \
fi \
else \
echo "Skipping Playwright installation (INSTALL_PLAYWRIGHT=${INSTALL_PLAYWRIGHT})."; \
fi
@echo "$(GREEN)Python dependencies installed successfully.$(RESET)"

View File

@@ -27,7 +27,7 @@ Welcome to OpenHands (formerly OpenDevin), a platform for software development a
OpenHands agents can do anything a human developer can: modify code, run commands, browse the web,
call APIs, and yes—even copy code snippets from StackOverflow.
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [Quick Start](#-quick-start).
Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or [sign up for OpenHands Cloud](https://app.all-hands.dev) to get started.
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
@@ -36,12 +36,21 @@ Learn more at [docs.all-hands.dev](https://docs.all-hands.dev), or jump to the [
![App screenshot](./docs/static/img/screenshot.png)
## ⚡ Quick Start
## ☁️ OpenHands Cloud
The easiest way to get started with OpenHands is on [OpenHands Cloud](https://app.all-hands.dev),
which comes with $50 in free credits for new users.
The easiest way to run OpenHands is in Docker.
## 💻 Running OpenHands Locally
OpenHands can also run on your local system using Docker.
See the [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) guide for
system requirements and more information.
> [!WARNING]
> On a public network? See our [Hardened Docker Installation Guide](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation)
> to secure your deployment by restricting network binding and implementing additional security measures.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.32-nikolaik
@@ -56,17 +65,21 @@ docker run -it --rm --pull=always \
docker.all-hands.dev/all-hands-ai/openhands:0.32
```
> [!WARNING]
> On a public network? See our [Hardened Docker Installation](https://docs.all-hands.dev/modules/usage/runtimes/docker#hardened-docker-installation) guide
> to secure your deployment by restricting network binding and implementing additional security measures.
You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)!
Finally, you'll need a model provider and API key.
When you open the application, you'll be asked to choose an LLM provider and add an API key.
[Anthropic's Claude 3.5 Sonnet](https://www.anthropic.com/api) (`anthropic/claude-3-5-sonnet-20241022`)
works best, but you have [many options](https://docs.all-hands.dev/modules/usage/llms).
---
## 💡 Other ways to run OpenHands
> [!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, please
> [get in touch with us](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
> for advanced deployment options.
You can also [connect OpenHands to your local filesystem](https://docs.all-hands.dev/modules/usage/runtimes/docker#connecting-to-your-filesystem),
run OpenHands in a scriptable [headless mode](https://docs.all-hands.dev/modules/usage/how-to/headless-mode),
@@ -75,14 +88,6 @@ or run it on tagged issues with [a github action](https://docs.all-hands.dev/mod
Visit [Running OpenHands](https://docs.all-hands.dev/modules/usage/installation) for more information and setup instructions.
> [!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 isolation or scalability.
>
> 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.
If you want to modify the OpenHands source code, check out [Development.md](https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md).
Having issues? The [Troubleshooting Guide](https://docs.all-hands.dev/modules/usage/troubleshooting) can help.

View File

@@ -216,13 +216,13 @@ model = "gpt-4o"
[agent]
# Whether the browsing tool is enabled
codeact_enable_browsing = true
enable_browsing = true
# Whether the LLM draft editor is enabled
codeact_enable_llm_editor = false
enable_llm_editor = false
# Whether the IPython tool is enabled
codeact_enable_jupyter = true
enable_jupyter = true
# LLM config group to use
#llm_config = 'your-llm-config-group'

View File

@@ -354,12 +354,12 @@ Les options de configuration de l'agent sont définies dans les sections `[agent
- Valeur par défaut : `true`
- Description : Si l'appel de fonction est activé
- `codeact_enable_browsing`
- `enable_browsing`
- Type : `bool`
- Valeur par défaut : `false`
- Description : Si le délégué de navigation est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Type : `bool`
- Valeur par défaut : `false`
- Description : Si l'éditeur LLM est activé dans l'espace d'action (fonctionne uniquement avec l'appel de fonction)

View File

@@ -348,12 +348,12 @@ dockerコマンドで使用する場合は、`-e LLM_<option>`として渡しま
- デフォルト値: `true`
- 説明: 関数呼び出しが有効かどうか
- `codeact_enable_browsing`
- `enable_browsing`
- 型: `bool`
- デフォルト値: `false`
- 説明: アクションスペースでブラウジングデリゲートが有効かどうか(関数呼び出しでのみ機能)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- 型: `bool`
- デフォルト値: `false`
- 説明: アクションスペースでLLMエディタが有効かどうか関数呼び出しでのみ機能

View File

@@ -21,8 +21,8 @@ OpenHandsがリポジトリで動作する際:
```
---
name: <Microagentの名前>
type: <MicroAgentのタイプ>
version: <MicroAgentのバージョン>
type: <Microagentのタイプ>
version: <Microagentのバージョン>
agent: <エージェントのタイプ (通常はCodeActAgent)>
triggers:
- <オプション: microagentをトリガーするキーワード。トリガーを削除すると、常に含まれるようになります>

View File

@@ -7,7 +7,7 @@ O GitHub Cloud Resolver automatiza correções de código e fornece assistência
## Configuração
O Resolvedor do GitHub na Nuvem está disponível automaticamente quando você
[concede acesso ao repositório do OpenHands Cloud](./openhands-cloud.md#adding-repository-access).
[concede acesso ao repositório do OpenHands Cloud](./openhands-cloud#adding-repository-access).
## Uso

View File

@@ -292,17 +292,17 @@ As opções de configuração do agente são definidas nas seções `[agent]` e
- Padrão: `true`
- Descrição: Se a chamada de função está habilitada
- `codeact_enable_browsing`
- `enable_browsing`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o delegado de navegação está habilitado no espaço de ação (funciona apenas com chamada de função)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o editor LLM está habilitado no espaço de ação (funciona apenas com chamada de função)
- `codeact_enable_jupyter`
- `enable_jupyter`
- Tipo: `bool`
- Padrão: `false`
- Descrição: Se o Jupyter está habilitado no espaço de ação

View File

@@ -21,8 +21,8 @@ Todos os microagentes usam arquivos markdown com frontmatter YAML que possuem in
```
---
name: <Nome do microagente>
type: <Tipo do MicroAgent>
version: <Versão do MicroAgent>
type: <Tipo do Microagent>
version: <Versão do Microagent>
agent: <O tipo de agente (normalmente CodeActAgent)>
triggers:
- <Palavras-chave opcionais que acionam o microagente. Se os gatilhos forem removidos, ele sempre será incluído>

View File

@@ -349,17 +349,17 @@ Agent 配置选项在 `config.toml` 文件的 `[agent]` 和 `[agent.<agent_name>
- 默认值: `true`
- 描述: 是否启用函数调用
- `codeact_enable_browsing`
- `enable_browsing`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用浏览代理(仅适用于函数调用)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用 LLM 编辑器(仅适用于函数调用)
- `codeact_enable_jupyter`
- `enable_jupyter`
- 类型: `bool`
- 默认值: `false`
- 描述: 是否在 action space 中启用 Jupyter

View File

@@ -5,7 +5,7 @@ The GitHub Resolver automates code fixes and provides intelligent assistance for
## Setup
The Cloud GitHub Resolver is available automatically when you
[grant OpenHands Cloud repository access](./openhands-cloud.md#adding-repository-access).
[grant OpenHands Cloud repository access](./openhands-cloud#adding-repository-access).
## Usage

View File

@@ -21,7 +21,10 @@ After visiting OpenHands Cloud, you will be asked to connect with your GitHub ac
You can grant OpenHands specific repository access:
1. Click the `Select a GitHub project` dropdown, select `Add more repositories...`.
2. Select the organization, then choose the specific repositories to grant OpenHands access to.
- Openhands requests short-lived tokens (8-hour expiry) with these permissions:
<details>
<summary>Permission Details for Repository Access</summary>
Openhands requests short-lived tokens (8-hour expiry) with these permissions:
- Actions: Read and write
- Administration: Read-only
- Commit statuses: Read and write
@@ -31,9 +34,12 @@ You can grant OpenHands specific repository access:
- Pull requests: Read and write
- Webhooks: Read and write
- Workflows: Read and write
- Repository access for a user is granted based on:
Repository access for a user is granted based on:
- Granted permission for the repository.
- User's GitHub permissions (owner/collaborator).
</details>
3. Click on `Install & Authorize`.
### Modifying Repository Access
@@ -41,3 +47,9 @@ You can grant OpenHands specific repository access:
You can modify repository access at any time by:
* Using the same `Select a GitHub project > Add more repositories` workflow, or
* Visiting the Settings page and selecting `Configure GitHub Repositories` under the `GitHub Settings` section.
## Conversation Persistence
- Conversations List Displays only the 10 most recent conversations initiated within the past 10 days.
- Workspaces Conversation workspaces are retained for 14 days.
- Runtimes Runtimes remain active ("warm") for 30 minutes. After this period, resuming a conversation may take 12 minutes.

View File

@@ -291,17 +291,17 @@ The agent configuration options are defined in the `[agent]` and `[agent.<agent_
- Default: `true`
- Description: Whether function calling is enabled
- `codeact_enable_browsing`
- `enable_browsing`
- Type: `bool`
- Default: `false`
- Description: Whether browsing delegate is enabled in the action space (only works with function calling)
- `codeact_enable_llm_editor`
- `enable_llm_editor`
- Type: `bool`
- Default: `false`
- Description: Whether LLM editor is enabled in the action space (only works with function calling)
- `codeact_enable_jupyter`
- `enable_jupyter`
- Type: `bool`
- Default: `false`
- Description: Whether Jupyter is enabled in the action space

View File

@@ -50,3 +50,6 @@ docker run -it \
```
This command will start an interactive session in Docker where you can input tasks and receive responses from OpenHands.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.

View File

@@ -47,6 +47,9 @@ docker run -it \
python -m openhands.core.main -t "write a bash script that prints hi"
```
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
## Advanced Headless Configurations
To view all available configuration options for headless mode, run the Python command with the `--help` flag.

View File

@@ -35,8 +35,8 @@ A useful feature is the ability to connect to your local filesystem. To mount yo
Be careful! There's nothing stopping the OpenHands agent from deleting or modifying
any files that are mounted into its workspace.
This setup can cause some issues with file permissions (hence the `SANDBOX_USER_ID` variable)
but seems to work well on most systems.
The `-e SANDBOX_USER_ID=$(id -u)` is passed to the Docker command to ensure the sandbox user matches the host users
permissions. This prevents the agent from creating root-owned files in the mounted workspace.
## Hardened Docker Installation

View File

@@ -20,3 +20,18 @@ Try these in order:
* If using Docker Desktop, ensure `Settings > Advanced > Allow the default Docker socket to be used` is enabled.
* Depending on your configuration you may need `Settings > Resources > Network > Enable host networking` enabled in Docker Desktop.
* Reinstall Docker Desktop.
### Permission Error
**Description**
On initial prompt, an error is seen with `Permission Denied` or `PermissionError`.
**Resolution**
* Check if the `~/.openhands-state` is owned by `root`. If so, you can:
* Change the directory's ownership: `sudo chown <user>:<user> ~/.openhands-state`.
* or update permissions on the directory: `sudo chmod 777 ~/.openhands-state`
* or delete it if you dont need previous data. OpenHands will recreate it. You'll need to re-enter LLM settings.
* If mounting a local directory, ensure your `WORKSPACE_BASE` has the necessary permissions for the user running
OpenHands.

View File

@@ -24,7 +24,7 @@
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.8.2"
"typescript": "~5.8.3"
},
"engines": {
"node": ">=18.0"
@@ -17638,9 +17638,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
"integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",

View File

@@ -31,7 +31,7 @@
"@docusaurus/module-type-aliases": "^3.5.1",
"@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.5.1",
"typescript": "~5.8.2"
"typescript": "~5.8.3"
},
"browserslist": {
"production": [

View File

@@ -9307,10 +9307,10 @@ typedarray-to-buffer@^3.1.5:
dependencies:
is-typedarray "^1.0.0"
typescript@~5.8.2:
version "5.8.2"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz"
integrity sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==
typescript@~5.8.3:
version "5.8.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e"
integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==
undici-types@~5.26.4:
version "5.26.5"

View File

@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -74,6 +75,7 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -22,6 +22,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -55,6 +56,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -61,6 +62,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
# copy 'draft_editor' config if exists

View File

@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -72,6 +73,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -22,6 +22,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -86,6 +87,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -50,6 +51,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -20,6 +20,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -131,10 +132,11 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -21,6 +21,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -76,11 +77,13 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
agent_config = AgentConfig(
function_calling=False,
codeact_enable_jupyter=True,
codeact_enable_browsing_delegate=True,
enable_jupyter=True,
enable_browsing=True,
)
config.set_agent_config(agent_config)
return config

View File

@@ -18,6 +18,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -66,6 +67,8 @@ def get_config(
else:
logger.info('Agent config not provided, using default settings')
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -54,6 +55,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -34,6 +34,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -75,6 +76,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -27,6 +27,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -96,6 +97,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -14,6 +14,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -63,6 +64,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -19,6 +19,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -121,6 +122,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -91,6 +92,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -84,39 +85,54 @@ I've already taken care of all changes to any of the test files described in the
Also the development Python environment is already set up for you (i.e., all dependencies already installed), so you don't need to install other packages.
Your task is to make the minimal changes to non-test files in the /workspace/{workspace_dir_name} directory to ensure the <issue_description> is satisfied.
Follow these steps to resolve the issue:
Follow these phases to resolve the issue:
1. EXPLORATION: First, thoroughly explore the repository structure using tools like `find` and `grep`.
- Identify all files mentioned in the problem statement
- Locate where the issue occurs in the codebase
- Understand the surrounding context and dependencies
- Use `grep` to search for relevant functions, classes, or error messages
Phase 1. READING: read the problem and reword it in clearer terms
1.1 If there are code or config snippets. Express in words any best practices or conventions in them.
1.2 Hightlight message errors, method names, variables, file names, stack traces, and technical details.
1.3 Explain the problem in clear terms.
1.4 Enumerate the steps to reproduce the problem.
1.5 Hightlight any best practices to take into account when testing and fixing the issue
2. ANALYSIS: Based on your exploration, think carefully about the problem and propose 2-5 possible approaches to fix the issue.
- Analyze the root cause of the problem
- Consider trade-offs between different solutions
- Select the most promising approach and explain your reasoning
Phase 2. RUNNING: install and run the tests on the repository
2.1 Follow the readme
2.2 Install the environment and anything needed
2.2 Iterate and figure out how to run the tests
3. TEST CREATION: Before implementing any fix, create a script to reproduce and verify the issue.
- Look at existing test files in the repository to understand the test format/structure
- Create a minimal reproduction script that demonstrates the issue
- Run your script to confirm the error exists
Phase 3. EXPLORATION: find the files that are related to the problem and possible solutions
3.1 Use `grep` to search for relevant methods, classes, keywords and error messages.
3.2 Identify all files related to the problem statement.
3.3 Propose the methods and files to fix the issue and explain why.
3.4 From the possible file locations, select the most likely location to fix the issue.
4. IMPLEMENTATION: Edit the source code to implement your chosen solution.
- Make minimal, focused changes to fix the issue
Phase 4. TEST CREATION: before implementing any fix, create a script to reproduce and verify the issue.
4.1 Look at existing test files in the repository to understand the test format/structure.
4.2 Create a minimal reproduction script that reproduces the located issue.
4.3 Run the reproduction script to confirm you are reproducing the issue.
4.4 Adjust the reproduction script as necessary.
5. VERIFICATION: Test your implementation thoroughly.
- Run your reproduction script to verify the fix works
- Add edge cases to your test script to ensure comprehensive coverage
- Run existing tests related to the modified code to ensure you haven't broken anything
Phase 5. FIX ANALYSIS: state clearly the problem and how to fix it
5.1 State clearly what the problem is.
5.2 State clearly where the problem is located.
5.3 State clearly how the test reproduces the issue.
5.4 State clearly the best practices to take into account in the fix.
5.5 State clearly how to fix the problem.
6. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
- Ensure you've fully addressed all requirements
- Run any tests in the repository related to:
* The issue you are fixing
* The files you modified
* The functions you changed
- If any tests fail, revise your implementation until all tests pass
Phase 6. FIX IMPLEMENTATION: Edit the source code to implement your chosen solution.
6.1 Make minimal, focused changes to fix the issue.
Phase 7. VERIFICATION: Test your implementation thoroughly.
7.1 Run your reproduction script to verify the fix works.
7.2 Add edge cases to your test script to ensure comprehensive coverage.
7.3 Run existing tests related to the modified code to ensure you haven't broken anything.
8. FINAL REVIEW: Carefully re-read the problem description and compare your changes with the base commit {instance["base_commit"]}.
8.1 Ensure you've fully addressed all requirements.
8.2 Run any tests in the repository related to:
8.2.1 The issue you are fixing
8.2.2 The files you modified
8.2.3 The functions you changed
8.3 If any tests fail, revise your implementation until all tests pass
Be thorough in your exploration, testing, and reasoning. It's fine if your thinking process is lengthy - quality and completeness are more important than brevity.
"""
@@ -210,12 +226,13 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config
@@ -223,6 +240,7 @@ def get_config(
def initialize_runtime(
runtime: Runtime,
instance: pd.Series, # this argument is not required
metadata: EvalMetadata,
):
"""Initialize the runtime for the agent.
@@ -562,7 +580,7 @@ def process_instance(
call_async_from_sync(runtime.connect)
try:
initialize_runtime(runtime, instance)
initialize_runtime(runtime, instance, metadata)
message_action = get_instruction(instance, metadata)

View File

@@ -30,6 +30,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
update_llm_config_for_completions_logging,
)
from openhands.controller.state.state import State
@@ -158,12 +159,13 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=False,
codeact_enable_browsing=RUN_WITH_BROWSING,
codeact_enable_llm_editor=False,
enable_jupyter=False,
enable_browsing=RUN_WITH_BROWSING,
enable_llm_editor=False,
condenser=metadata.condenser_config,
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -13,7 +13,10 @@ from typing import List
import yaml
from browsing import pre_login
from evaluation.utils.shared import get_default_sandbox_config_for_eval
from evaluation.utils.shared import (
get_default_sandbox_config_for_eval,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
AppConfig,
@@ -58,12 +61,14 @@ def get_config(
)
config.set_llm_config(llm_config)
if agent_config:
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
else:
logger.info('Agent config not provided, using default settings')
agent_config = AgentConfig(
enable_prompt_extensions=False,
)
agent_config = update_agent_config_for_eval(agent_config)
config.set_agent_config(agent_config)
return config

View File

@@ -15,6 +15,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -55,6 +56,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -16,6 +16,7 @@ from evaluation.utils.shared import (
prepare_dataset,
reset_logger_for_multiprocessing,
run_evaluation,
update_agent_config_for_eval,
)
from openhands.controller.state.state import State
from openhands.core.config import (
@@ -76,6 +77,8 @@ def get_config(
)
config.set_llm_config(metadata.llm_config)
agent_config = config.get_agent_config(metadata.agent_class)
agent_config = update_agent_config_for_eval(agent_config)
agent_config.enable_prompt_extensions = False
return config

View File

@@ -62,9 +62,9 @@ def get_config(
)
)
agent_config = AgentConfig(
codeact_enable_jupyter=True,
codeact_enable_browsing=True,
codeact_enable_llm_editor=False,
enable_jupyter=True,
enable_browsing=True,
enable_llm_editor=False,
)
config.set_agent_config(agent_config)
return config

View File

@@ -160,6 +160,26 @@ def cleanup():
process.join()
def update_agent_config_for_eval(
agent_config: AgentConfig | None = None,
) -> AgentConfig:
"""Update agent config with evaluation-specific settings.
Args:
agent_config: The agent config to update. If None, a new AgentConfig will be created.
Returns:
The updated agent config.
"""
if agent_config is None:
agent_config = AgentConfig()
# Note: We're not disabling repository memory here as requested
# This function can be used for other evaluation-specific settings
return agent_config
def make_metadata(
llm_config: LLMConfig,
dataset_name: str,
@@ -172,6 +192,8 @@ def make_metadata(
agent_config: AgentConfig | None = None,
condenser_config: CondenserConfig | None = None,
) -> EvalMetadata:
# Update agent config with evaluation-specific settings
agent_config = update_agent_config_for_eval(agent_config)
model_name = llm_config.model.split('/')[-1]
model_path = model_name.replace(':', '_').replace('@', '-')
eval_note = f'_N_{eval_note}' if eval_note else ''

View File

@@ -1,7 +1,7 @@
{
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
"project": "./tsconfig.json",
},
"extends": [
"airbnb",
@@ -11,15 +11,12 @@
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@tanstack/query/recommended"
],
"plugins": [
"prettier"
"plugin:@tanstack/query/recommended",
],
"plugins": ["prettier", "unused-imports"],
"rules": {
"prettier/prettier": [
"error"
],
"unused-imports/no-unused-imports": "error",
"prettier/prettier": ["error"],
// Resolves https://stackoverflow.com/questions/59265981/typescript-eslint-missing-file-extension-ts-import-extensions/59268871#59268871
"import/extensions": [
"error",
@@ -27,32 +24,26 @@
{
"": "never",
"ts": "never",
"tsx": "never"
}
]
"tsx": "never",
},
],
},
"settings": {
"react": {
"version": "detect"
}
"version": "detect",
},
},
"overrides": [
{
"files": [
"*.ts",
"*.tsx"
],
"files": ["*.ts", "*.tsx"],
"rules": {
// Allow state modification in reduce and Redux reducers
"no-param-reassign": [
"error",
{
"props": true,
"ignorePropertyModificationsFor": [
"acc",
"state"
]
}
"ignorePropertyModificationsFor": ["acc", "state"],
},
],
// For https://stackoverflow.com/questions/55844608/stuck-with-eslint-error-i-e-separately-loops-should-be-avoided-in-favor-of-arra
"no-restricted-syntax": "off",
@@ -66,24 +57,19 @@
2,
{
"required": {
"some": [
"nesting",
"id"
]
}
}
"some": ["nesting", "id"],
},
},
],
"react/prop-types": "off",
"react/no-array-index-key": "off",
"react-hooks/exhaustive-deps": "off",
"import/no-extraneous-dependencies": "off",
"react/react-in-jsx-scope": "off"
"react/react-in-jsx-scope": "off",
},
"parserOptions": {
"project": [
"**/tsconfig.json"
]
}
}
]
"project": ["**/tsconfig.json"],
},
},
],
}

View File

@@ -1,4 +1,3 @@
cd frontend
npm run check-unlocalized-strings
npx lint-staged
npm test

View File

@@ -37,4 +37,4 @@ describe("CopyToClipboardButton", () => {
const button = screen.getByTestId("copy-to-clipboard");
expect(button).toHaveAttribute("aria-label", "BUTTON$COPIED");
});
});
});

View File

@@ -217,6 +217,17 @@ describe("ChatInput", () => {
expect(onImagePaste).toHaveBeenCalledWith([file]);
});
it("should use the default maxRows value", () => {
// We can't directly test the maxRows prop as it's not exposed in the DOM
// Instead, we'll verify the component renders with the default props
render(<ChatInput onSubmit={onSubmitMock} />);
const textarea = screen.getByRole("textbox");
expect(textarea).toBeInTheDocument();
// The actual verification of maxRows=16 is handled internally by the TextareaAutosize component
// and affects how many rows the textarea can expand to
});
it("should not submit when Enter is pressed during IME composition", async () => {
const user = userEvent.setup();
render(<ChatInput onSubmit={onSubmitMock} />);

View File

@@ -76,11 +76,11 @@ describe("ConversationCard", () => {
const card = screen.getByTestId("conversation-card");
within(card).getByText("Conversation 1");
// Just check that the card contains the expected text content
expect(card).toHaveTextContent("Created");
expect(card).toHaveTextContent("ago");
// Use a regex to match the time part since it might have whitespace
const timeRegex = new RegExp(formatTimeDelta(new Date("2021-10-01T12:00:00Z")));
expect(card).toHaveTextContent(timeRegex);

View File

@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { handleObservationMessage } from "#/services/observations";
import store from "#/store";
import { ObservationMessage } from "#/types/message";
// Mock dependencies
vi.mock("#/store", () => ({
default: {
dispatch: vi.fn(),
},
}));
describe("Observations Service", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("handleObservationMessage", () => {
const createErrorMessage = (): ObservationMessage => ({
id: 14,
timestamp: "2025-04-14T13:37:54.451843",
message: "The action has not been executed.",
cause: 12,
observation: "error",
content: "The action has not been executed.",
extras: {
error_id: "",
metadata: {},
},
});
it("should dispatch error messages exactly once", () => {
const errorMessage = createErrorMessage();
handleObservationMessage(errorMessage);
expect(store.dispatch).toHaveBeenCalledTimes(1);
expect(store.dispatch).toHaveBeenCalledWith({
type: "chat/addAssistantObservation",
payload: expect.objectContaining({
observation: "error",
content: "The action has not been executed.",
source: "user",
extras: {
error_id: "",
},
}),
});
});
});
});

View File

@@ -83,6 +83,7 @@
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.0.0",
"lint-staged": "^15.5.0",
@@ -9173,6 +9174,22 @@
"semver": "bin/semver.js"
}
},
"node_modules/eslint-plugin-unused-imports": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-4.1.4.tgz",
"integrity": "sha512-YptD6IzQjDardkl0POxnnRBhU1OEePMV0nd6siHaRBbd+lyh6NAhFEobiznKU7kTsSsDeSD62Pe7kAM1b7dAZQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "^8.0.0-0 || ^7.0.0 || ^6.0.0 || ^5.0.0",
"eslint": "^9.0.0 || ^8.0.0"
},
"peerDependenciesMeta": {
"@typescript-eslint/eslint-plugin": {
"optional": true
}
}
},
"node_modules/eslint-scope": {
"version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",

View File

@@ -53,7 +53,7 @@
"dev": "npm run make-i18n && cross-env VITE_MOCK_API=false react-router dev",
"dev:mock": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=false react-router dev",
"dev:mock:saas": "npm run make-i18n && cross-env VITE_MOCK_API=true VITE_MOCK_SAAS=true react-router dev",
"build": "npm run make-i18n && npm run typecheck && react-router build",
"build": "npm run make-i18n && react-router build",
"start": "npx sirv-cli build/ --single",
"test": "vitest run",
"test:e2e": "playwright test",
@@ -62,7 +62,7 @@
"preview": "vite preview",
"make-i18n": "node scripts/make-i18n-translations.cjs",
"prelint": "npm run make-i18n",
"lint": "eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
"lint": "npm run typecheck && eslint src --ext .ts,.tsx,.js && prettier --check src/**/*.{ts,tsx}",
"lint:fix": "eslint src --ext .ts,.tsx,.js --fix && prettier --write src/**/*.{ts,tsx}",
"prepare": "cd .. && husky frontend/.husky",
"typecheck": "react-router typegen && tsc",
@@ -107,6 +107,7 @@
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-unused-imports": "^4.1.4",
"husky": "^9.1.7",
"jsdom": "^26.0.0",
"lint-staged": "^15.5.0",

View File

@@ -47,6 +47,7 @@ const SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx"];
// Attributes that typically don't contain user-facing text
const NON_TEXT_ATTRIBUTES = [
"className",
"i18nKey",
"testId",
"id",
"name",
@@ -685,23 +686,23 @@ function scanDirectoryForUnlocalizedStrings(dirPath) {
try {
const srcPath = path.resolve(__dirname, '../src');
console.log('Checking for unlocalized strings in frontend code...');
// Get unlocalized strings using the AST scanner
const results = scanDirectoryForUnlocalizedStrings(srcPath);
// If we found any unlocalized strings, format them for output and exit with error
if (results.size > 0) {
const formattedResults = Array.from(results.entries())
.map(([file, strings]) => `\n${file}:\n ${strings.join('\n ')}`)
.join('\n');
console.error(`Error: Found unlocalized strings in the following files:${formattedResults}`);
process.exit(1);
}
console.log('✅ No unlocalized strings found in frontend code.');
process.exit(0);
} catch (error) {
console.error('Error running unlocalized strings check:', error);
process.exit(1);
}
}

View File

@@ -29,7 +29,7 @@ export function ChatInput({
disabled,
showButton = true,
value,
maxRows = 4,
maxRows = 16,
onSubmit,
onStop,
onChange,

View File

@@ -48,7 +48,7 @@ export function ChatMessage({
"rounded-xl relative",
"flex flex-col gap-2",
type === "user" && " max-w-[305px] p-4 bg-tertiary self-end",
type === "assistant" && "mt-6 max-w-full bg-tranparent",
type === "assistant" && "mt-6 max-w-full bg-transparent",
)}
>
<CopyToClipboardButton

View File

@@ -47,7 +47,7 @@ export function AgentStatusBar() {
});
return;
}
if (curAgentState === AgentState.LOADING && message.trim()) {
if (message.trim()) {
setStatusMessage(message);
} else {
setStatusMessage(AGENT_STATUS_MAP[curAgentState].message);

View File

@@ -4961,19 +4961,19 @@
"tr": "Gezinme tamamlandı"
},
"OBSERVATION_MESSAGE$RECALL": {
"en": "MicroAgent Activated",
"en": "Microagent Activated",
"ja": "マイクロエージェントが有効化されました",
"zh-CN": "微代理已激活",
"zh-TW": "微代理已啟動",
"ko-KR": "마이크로에이전트 활성화됨",
"no": "MikroAgent aktivert",
"it": "MicroAgent attivato",
"pt": "MicroAgent ativado",
"es": "MicroAgent activado",
"it": "Microagent attivato",
"pt": "Microagent ativado",
"es": "Microagent activado",
"ar": "تم تنشيط الوكيل المصغر",
"fr": "MicroAgent activé",
"fr": "Microagent activé",
"tr": "MikroAjan Etkinleştirildi",
"de": "MicroAgent aktiviert"
"de": "Microagent aktiviert"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
@@ -6089,4 +6089,4 @@
"tr": "belgelendirme",
"de": "Dokumentation"
}
}
}

View File

@@ -124,7 +124,7 @@ function AccountSettings() {
formData.get("enable-memory-condenser-switch")?.toString() === "on";
const enableSoundNotifications =
formData.get("enable-sound-notifications-switch")?.toString() === "on";
const llmBaseUrl = formData.get("base-url-input")?.toString() || "";
const llmBaseUrl = formData.get("base-url-input")?.toString().trim() || "";
const inputApiKey = formData.get("llm-api-key-input")?.toString() || "";
const llmApiKey =
inputApiKey === "" && isLLMKeySet

View File

@@ -121,7 +121,7 @@ function AppContent() {
function renderMain() {
if (width <= 640) {
return (
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full">
<div className="rounded-xl overflow-hidden border border-neutral-600 w-full bg-base-secondary">
<ChatInterface />
</div>
);

View File

@@ -104,7 +104,12 @@ export function handleActionMessage(message: ActionMessage) {
}
if (message.source === "agent") {
if (message.args && message.args.thought) {
// Only add thought as a message if it's not a "think" action
if (
message.args &&
message.args.thought &&
message.action !== ActionType.THINK
) {
store.dispatch(addAssistantMessage(message.args.thought));
}
// Need to convert ActionMessage to RejectAction

View File

@@ -52,6 +52,7 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.THINK:
case ObservationType.NULL:
case ObservationType.RECALL:
case ObservationType.ERROR:
break; // We don't display the default message for these observations
default:
store.dispatch(addAssistantMessage(message.message));

View File

@@ -24,6 +24,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
"browse_interactive",
"edit",
"recall",
"think",
];
function getRiskText(risk: ActionSecurityRisk) {

View File

@@ -32,6 +32,9 @@ enum ObservationType {
// An observation that shows agent's context extension
RECALL = "recall",
// An error observation
ERROR = "error",
// A no-op observation
NULL = "null",
}

View File

@@ -1,3 +1,5 @@
import { downloadJSON } from "./download-json";
function isSaveFilePickerSupported(): boolean {
return typeof window !== "undefined" && "showSaveFilePicker" in window;
}
@@ -6,25 +8,37 @@ export async function downloadTrajectory(
conversationId: string,
data: unknown[] | null,
): Promise<void> {
if (!isSaveFilePickerSupported()) {
throw new Error(
"Your browser doesn't support downloading files. Please use Chrome, Edge, or another browser that supports the File System Access API.",
);
}
const options: SaveFilePickerOptions = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File", // This is a file type description, not user-facing text
accept: {
"application/json": [".json"],
},
},
],
};
// Ensure data is an object for downloadJSON
const jsonData = data || {};
const fileHandle = await window.showSaveFilePicker(options);
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
if (!isSaveFilePickerSupported()) {
// Fallback for browsers that don't support File System Access API (Safari, Firefox)
// This method dumps the JSON data right into the download folder
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
return;
}
try {
const options: SaveFilePickerOptions = {
suggestedName: `trajectory-${conversationId}.json`,
types: [
{
description: "JSON File", // This is a file type description, not user-facing text
accept: {
"application/json": [".json"],
},
},
],
};
const fileHandle = await window.showSaveFilePicker(options);
const writable = await fileHandle.createWritable();
await writable.write(JSON.stringify(data, null, 2));
await writable.close();
} catch (error) {
// If an error occurs, fall back to the downloadJSON method
if (error instanceof Error && error.name !== "AbortError") {
downloadJSON(jsonData as object, `trajectory-${conversationId}.json`);
}
}
}

View File

@@ -1,6 +1,6 @@
# OpenHands MicroAgents
# OpenHands Microagents
MicroAgents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
Microagents are specialized prompts that enhance OpenHands with domain-specific knowledge and task-specific workflows. They help developers by providing expert guidance, automating common tasks, and ensuring consistent practices across projects. Each microagent is designed to excel in a specific area, from Git operations to code review processes.
## Sources of Microagents
@@ -49,7 +49,7 @@ When OpenHands works with a repository, it:
2. Loads relevant knowledge agents based on keywords in conversations
3. Enable task agent if user select one of them
## Types of MicroAgents
## Types of Microagents
Most microagents use markdown files with YAML frontmatter. For repository agents (repo.md), the frontmatter is optional - if not provided, the file will be loaded with default settings as a repository agent.

View File

@@ -30,6 +30,6 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
git remote -v && git branch # to find the current org, repo and branch
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Authorization: Bearer $GITLAB_TOKEN" \
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
```

View File

@@ -53,9 +53,9 @@ The agent provides several built-in tools:
## Configuration
Tools can be enabled/disabled through configuration parameters:
- `codeact_enable_browsing`: Enable browser interaction tools
- `codeact_enable_jupyter`: Enable IPython code execution
- `codeact_enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
- `enable_browsing`: Enable browser interaction tools
- `enable_jupyter`: Enable IPython code execution
- `enable_llm_editor`: Enable LLM-based file editing (falls back to string replacement editor if disabled)
## Micro-agents

View File

@@ -62,21 +62,21 @@ class CodeActAgent(Agent):
Parameters:
- llm (LLM): The llm to be used by this agent
- config (AgentConfig): The configuration for this agent
"""
super().__init__(llm, config)
self.pending_actions: deque[Action] = deque()
self.reset()
# Retrieve the enabled tools
self.tools = codeact_function_calling.get_tools(
codeact_enable_browsing=self.config.codeact_enable_browsing,
codeact_enable_jupyter=self.config.codeact_enable_jupyter,
codeact_enable_llm_editor=self.config.codeact_enable_llm_editor,
built_in_tools = codeact_function_calling.get_tools(
enable_browsing=self.config.enable_browsing,
enable_jupyter=self.config.enable_jupyter,
enable_llm_editor=self.config.enable_llm_editor,
llm=self.llm,
)
logger.debug(
f"TOOLS loaded for CodeActAgent: {', '.join([tool.get('function').get('name') for tool in self.tools])}"
)
self.tools = built_in_tools
self.prompt_manager = PromptManager(
prompt_dir=os.path.join(os.path.dirname(__file__), 'prompts'),
)
@@ -137,10 +137,23 @@ class CodeActAgent(Agent):
'messages': self.llm.format_messages_for_llm(messages),
}
params['tools'] = self.tools
if self.mcp_tools:
# Only add tools with unique names
existing_names = {tool['function']['name'] for tool in params['tools']}
unique_mcp_tools = [
tool
for tool in self.mcp_tools
if tool['function']['name'] not in existing_names
]
params['tools'] += unique_mcp_tools
# log to litellm proxy if possible
params['extra_body'] = {'metadata': state.to_llm_metadata(agent_name=self.name)}
response = self.llm.completion(**params)
logger.debug(f'Response from LLM: {response}')
actions = codeact_function_calling.response_to_actions(response)
logger.debug(f'Actions after response_to_actions: {actions}')
for action in actions:
self.pending_actions.append(action)
return self.pending_actions.popleft()

View File

@@ -24,6 +24,7 @@ from openhands.core.exceptions import (
FunctionCallNotExistsError,
FunctionCallValidationError,
)
from openhands.core.logger import openhands_logger as logger
from openhands.events.action import (
Action,
AgentDelegateAction,
@@ -37,9 +38,11 @@ from openhands.events.action import (
IPythonRunCellAction,
MessageAction,
)
from openhands.events.action.mcp import McpAction
from openhands.events.event import FileEditSource, FileReadSource
from openhands.events.tool import ToolCallMetadata
from openhands.llm import LLM
from openhands.mcp import MCPClientTool
def combine_thought(action: Action, thought: str) -> Action:
@@ -70,6 +73,7 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
# Process each tool call to OpenHands action
for i, tool_call in enumerate(assistant_msg.tool_calls):
action: Action
logger.debug(f'Tool call in function_calling.py: {tool_call}')
try:
arguments = json.loads(tool_call.function.arguments)
except json.decoder.JSONDecodeError as e:
@@ -191,6 +195,15 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])
# ================================================
# McpAction (MCP)
# ================================================
elif tool_call.function.name.endswith(MCPClientTool.postfix()):
action = McpAction(
name=tool_call.function.name.rstrip(MCPClientTool.postfix()),
arguments=tool_call.function.arguments,
)
else:
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
@@ -227,9 +240,9 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
def get_tools(
codeact_enable_browsing: bool = False,
codeact_enable_llm_editor: bool = False,
codeact_enable_jupyter: bool = False,
enable_browsing: bool = False,
enable_llm_editor: bool = False,
enable_jupyter: bool = False,
llm: LLM | None = None,
) -> list[ChatCompletionToolParam]:
SIMPLIFIED_TOOL_DESCRIPTION_LLM_SUBSTRS = ['gpt-', 'o3', 'o1']
@@ -246,12 +259,12 @@ def get_tools(
ThinkTool,
FinishTool,
]
if codeact_enable_browsing:
if enable_browsing:
tools.append(WebReadTool)
tools.append(BrowserTool)
if codeact_enable_jupyter:
if enable_jupyter:
tools.append(IPythonTool)
if codeact_enable_llm_editor:
if enable_llm_editor:
tools.append(LLMBasedFileEditTool)
else:
tools.append(

View File

@@ -37,6 +37,7 @@ class Agent(ABC):
self.config = config
self._complete = False
self.prompt_manager: 'PromptManager' | None = None
self.mcp_tools: list[dict] = []
@property
def complete(self) -> bool:
@@ -111,3 +112,11 @@ class Agent(ABC):
if not bool(cls._registry):
raise AgentNotRegisteredError()
return list(cls._registry.keys())
def set_mcp_tools(self, mcp_tools: list[dict]) -> None:
"""Sets the list of MCP tools for the agent.
Args:
- mcp_tools (list[dict]): The list of MCP tools.
"""
self.mcp_tools = mcp_tools

View File

@@ -66,7 +66,7 @@ from openhands.events.observation import (
)
from openhands.events.serialization.event import event_to_trajectory, truncate_content
from openhands.llm.llm import LLM
from openhands.llm.metrics import Metrics, TokenUsage
from openhands.llm.metrics import Metrics
# note: RESUME is only available on web GUI
TRAFFIC_CONTROL_REMINDER = (
@@ -1114,36 +1114,38 @@ class AgentController:
To avoid performance issues with long conversations, we only keep:
- accumulated_cost: The current total cost
- latest token_usage: Token statistics from the most recent API call
- accumulated_token_usage: Accumulated token statistics across all API calls
Args:
action: The action to attach metrics to
"""
# Create a minimal metrics object with just what the frontend needs
metrics = Metrics(model_name=self.agent.llm.metrics.model_name)
metrics.accumulated_cost = self.agent.llm.metrics.accumulated_cost
if self.agent.llm.metrics.token_usages:
latest_usage = self.agent.llm.metrics.token_usages[-1]
metrics.add_token_usage(
prompt_tokens=latest_usage.prompt_tokens,
completion_tokens=latest_usage.completion_tokens,
cache_read_tokens=latest_usage.cache_read_tokens,
cache_write_tokens=latest_usage.cache_write_tokens,
response_id=latest_usage.response_id,
)
metrics._accumulated_token_usage = (
self.agent.llm.metrics.accumulated_token_usage
)
action.llm_metrics = metrics
# Log the metrics information for frontend display
log_usage: TokenUsage | None = (
metrics.token_usages[-1] if metrics.token_usages else None
)
# Log the metrics information for debugging
# Get the latest usage directly from the agent's metrics
latest_usage = None
if self.agent.llm.metrics.token_usages:
latest_usage = self.agent.llm.metrics.token_usages[-1]
accumulated_usage = self.agent.llm.metrics.accumulated_token_usage
self.log(
'debug',
f'Action metrics - accumulated_cost: {metrics.accumulated_cost}, '
f'tokens (prompt/completion/cache_read/cache_write): '
f'{log_usage.prompt_tokens if log_usage else 0}/'
f'{log_usage.completion_tokens if log_usage else 0}/'
f'{log_usage.cache_read_tokens if log_usage else 0}/'
f'{log_usage.cache_write_tokens if log_usage else 0}',
f'latest tokens (prompt/completion/cache_read/cache_write): '
f'{latest_usage.prompt_tokens if latest_usage else 0}/'
f'{latest_usage.completion_tokens if latest_usage else 0}/'
f'{latest_usage.cache_read_tokens if latest_usage else 0}/'
f'{latest_usage.cache_write_tokens if latest_usage else 0}, '
f'accumulated tokens (prompt/completion): '
f'{accumulated_usage.prompt_tokens}/'
f'{accumulated_usage.completion_tokens}',
extra={'msg_type': 'METRICS'},
)

View File

@@ -1,13 +1,27 @@
import asyncio
import logging
import sys
import time
from pathlib import Path
from typing import List, Optional
from uuid import uuid4
import toml
from prompt_toolkit import PromptSession, print_formatted_text
from prompt_toolkit.formatted_text import FormattedText
from prompt_toolkit.application import Application
from prompt_toolkit.completion import Completer, Completion
from prompt_toolkit.formatted_text import HTML, FormattedText
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout.containers import HSplit, Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout.layout import Layout
from prompt_toolkit.patch_stdout import patch_stdout
from prompt_toolkit.shortcuts import clear, print_container
from prompt_toolkit.styles import Style
from prompt_toolkit.widgets import Frame, TextArea
import openhands.agenthub # noqa F401 (we import this to get the agents registered)
from openhands import __version__
from openhands.core.config import (
AppConfig,
parse_arguments,
@@ -37,88 +51,176 @@ from openhands.events.observation import (
AgentStateChangedObservation,
CmdOutputObservation,
FileEditObservation,
FileReadObservation,
)
from openhands.io import read_task
from openhands.llm.metrics import Metrics
from openhands.mcp import fetch_mcp_tools_from_config
from openhands.microagent.microagent import BaseMicroagent
prompt_session = PromptSession()
# Color and styling constants
COLOR_GOLD = '#FFD700'
COLOR_GREY = '#808080'
DEFAULT_STYLE = Style.from_dict(
{
'gold': COLOR_GOLD,
'grey': COLOR_GREY,
'prompt': f'{COLOR_GOLD} bold',
}
)
COMMANDS = {
'/exit': 'Exit the application',
'/help': 'Display available commands',
'/init': 'Initialize a new repository',
}
REPO_MD_CREATE_PROMPT = """
Please explore this repository. Create the file .openhands/microagents/repo.md with:
- A description of the project
- An overview of the file structure
- Any information on how to run tests or other relevant commands
- Any other information that would be helpful to a brand new developer
Keep it short--just a few paragraphs will do.
"""
def display_message(message: str) -> None:
class CommandCompleter(Completer):
"""Custom completer for commands."""
def get_completions(self, document, complete_event):
text = document.text
# Only show completions if the user has typed '/'
if text.startswith('/'):
# If just '/' is typed, show all commands
if text == '/':
for command, description in COMMANDS.items():
yield Completion(
command[1:], # Remove the leading '/' as it's already typed
start_position=0,
display=f'{command} - {description}',
)
# Otherwise show matching commands
else:
for command, description in COMMANDS.items():
if command.startswith(text):
yield Completion(
command[len(text) :], # Complete the remaining part
start_position=0,
display=f'{command} - {description}',
)
class UsageMetrics:
def __init__(self):
self.total_cost: float = 0.00
self.total_input_tokens: int = 0
self.total_output_tokens: int = 0
self.total_cache_read: int = 0
self.total_cache_write: int = 0
prompt_session = PromptSession(style=DEFAULT_STYLE, completer=CommandCompleter())
def display_message(message: str):
message = message.strip()
if message:
print_formatted_text(f'\n{message}\n')
def display_command(command: str):
container = Frame(
TextArea(
text=command,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Run',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_confirmation(confirmation_state: ActionConfirmationStatus):
status_map = {
ActionConfirmationStatus.CONFIRMED: ('ansigreen', ''),
ActionConfirmationStatus.REJECTED: ('ansired', ''),
ActionConfirmationStatus.AWAITING_CONFIRMATION: ('ansiyellow', ''),
}
color, icon = status_map.get(confirmation_state, ('ansiyellow', ''))
print_formatted_text(
FormattedText(
[
('ansiyellow', '🤖 '),
('ansiyellow', message),
(color, f'{icon} '),
(color, str(confirmation_state)),
('', '\n'),
]
)
)
def display_command(command: str) -> None:
print_formatted_text(
FormattedText(
[
('', ' '),
('ansigreen', command),
('', '\n'),
]
)
)
def display_confirmation(confirmation_state: ActionConfirmationStatus) -> None:
if confirmation_state == ActionConfirmationStatus.CONFIRMED:
print_formatted_text(
FormattedText(
[
('ansigreen', ''),
('ansigreen', str(confirmation_state)),
('', '\n'),
]
)
)
elif confirmation_state == ActionConfirmationStatus.REJECTED:
print_formatted_text(
FormattedText(
[
('ansired', ''),
('ansired', str(confirmation_state)),
('', '\n'),
]
)
)
else:
print_formatted_text(
FormattedText(
[
('ansiyellow', ''),
('ansiyellow', str(confirmation_state)),
('', '\n'),
]
)
)
def display_command_output(output: str) -> None:
def display_command_output(output: str):
lines = output.split('\n')
formatted_lines = []
for line in lines:
if line.startswith('[Python Interpreter') or line.startswith('openhands@'):
# TODO: clean this up once we clean up terminal output
continue
print_formatted_text(FormattedText([('ansiblue', line)]))
formatted_lines.append(line)
formatted_lines.append('\n')
# Remove the last newline if it exists
if formatted_lines:
formatted_lines.pop()
container = Frame(
TextArea(
text=''.join(formatted_lines),
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Command Output',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_edit(event: FileEditAction | FileEditObservation) -> None:
print_formatted_text(
FormattedText(
[
('ansigreen', str(event)),
('', '\n'),
]
)
def display_file_edit(event: FileEditAction | FileEditObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Edit',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_file_read(event: FileReadObservation):
container = Frame(
TextArea(
text=f'{event}',
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='File Read',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('')
def display_event(event: Event, config: AppConfig) -> None:
@@ -136,10 +238,115 @@ def display_event(event: Event, config: AppConfig) -> None:
display_file_edit(event)
if isinstance(event, FileEditObservation):
display_file_edit(event)
if isinstance(event, FileReadObservation):
display_file_read(event)
if hasattr(event, 'confirmation_state') and config.security.confirmation_mode:
display_confirmation(event.confirmation_state)
def display_help(style=DEFAULT_STYLE):
print_formatted_text(
HTML(f'\n<grey>OpenHands CLI v{__version__}</grey>\n'), style=style
)
print_formatted_text(
HTML(
'<gold>OpenHands CLI lets you interact with the OpenHands agent from the command line.</gold>'
)
)
print_formatted_text('')
print_formatted_text('Things that you can try:')
print_formatted_text(
HTML('• Ask questions about the codebase <grey>> How does main.py work?</grey>')
)
print_formatted_text(
HTML(
'• Edit files or add new features <grey>> Add a new function to ...</grey>'
)
)
print_formatted_text(
HTML('• Find and fix issues <grey>> Fix the type error in ...</grey>')
)
print_formatted_text('')
print_formatted_text('Some tips to get the most out of OpenHands:')
print_formatted_text(
'• Be as specific as possible about the desired outcome or the problem to be solved.'
)
print_formatted_text(
'• Provide context, including relevant file paths and line numbers if available.'
)
print_formatted_text('• Break large tasks into smaller, manageable prompts.')
print_formatted_text('• Include relevant error messages or logs.')
print_formatted_text(
'• Specify the programming language or framework, if not obvious.'
)
print_formatted_text('')
print_formatted_text(HTML('Interactive commands:'), style=style)
for command, description in COMMANDS.items():
print_formatted_text(
HTML(f'<gold><b>{command}</b></gold> - <grey>{description}</grey>'),
style=style,
)
print_formatted_text('')
print_formatted_text(
HTML(
'<grey>Learn more at: https://docs.all-hands.dev/modules/usage/getting-started</grey>'
)
)
print_formatted_text('')
def display_banner(session_id: str, is_loaded: asyncio.Event):
print_formatted_text(
HTML(r"""<gold>
___ _ _ _
/ _ \ _ __ ___ _ __ | | | | __ _ _ __ __| |___
| | | | '_ \ / _ \ '_ \| |_| |/ _` | '_ \ / _` / __|
| |_| | |_) | __/ | | | _ | (_| | | | | (_| \__ \
\___ /| .__/ \___|_| |_|_| |_|\__,_|_| |_|\__,_|___/
|_|
</gold>"""),
style=DEFAULT_STYLE,
)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
banner_text = (
'Initialized session' if is_loaded.is_set() else 'Initializing session'
)
print_formatted_text(HTML(f'\n<grey>{banner_text} {session_id}</grey>\n'))
def display_welcome_message():
print_formatted_text(
HTML("<gold>Let's start building!</gold>\n"), style=DEFAULT_STYLE
)
print_formatted_text(
HTML('What do you want to build? <grey>Type /help for help</grey>\n'),
style=DEFAULT_STYLE,
)
def display_initialization_animation(text, is_loaded: asyncio.Event):
ANIMATION_FRAMES = ['', '', '', '', '', '', '', '', '', '']
i = 0
while not is_loaded.is_set():
sys.stdout.write('\n')
sys.stdout.write(
f'\033[s\033[J\033[38;2;255;215;0m[{ANIMATION_FRAMES[i % len(ANIMATION_FRAMES)]}] {text}\033[0m\033[u\033[1A'
)
sys.stdout.flush()
time.sleep(0.1)
i += 1
sys.stdout.write('\r' + ' ' * (len(text) + 10) + '\r')
sys.stdout.flush()
async def read_prompt_input(multiline=False):
try:
if multiline:
@@ -149,35 +356,292 @@ async def read_prompt_input(multiline=False):
def _(event):
event.current_buffer.validate_and_handle()
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
with patch_stdout():
message = await prompt_session.prompt_async(
'Enter your message and press Ctrl+D to finish:\n',
multiline=True,
key_bindings=kb,
)
else:
message = await prompt_session.prompt_async(
'>> ',
)
with patch_stdout():
message = await prompt_session.prompt_async(
'> ',
)
return message
except KeyboardInterrupt:
return 'exit'
return '/exit'
except EOFError:
return 'exit'
return '/exit'
async def read_confirmation_input():
try:
confirmation = await prompt_session.prompt_async(
'Confirm action (possible security risk)? (y/n) >> ',
'Confirm action (possible security risk)? (y/n) > ',
)
return confirmation.lower() == 'y'
except (KeyboardInterrupt, EOFError):
return False
async def main(loop: asyncio.AbstractEventLoop) -> None:
async def init_repository(current_dir: str) -> bool:
repo_file_path = Path(current_dir) / '.openhands' / 'microagents' / 'repo.md'
init_repo = False
if repo_file_path.exists():
try:
content = await asyncio.get_event_loop().run_in_executor(
None, read_file, repo_file_path
)
print_formatted_text(
'Repository instructions file (repo.md) already exists.\n'
)
container = Frame(
TextArea(
text=content,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Repository Instructions (repo.md)',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text('') # Add a newline after the frame
init_repo = cli_confirm(
'Do you want to re-initialize?',
['Yes, re-initialize', 'No, dismiss'],
)
if init_repo:
write_to_file(repo_file_path, '')
except Exception:
print_formatted_text('Error reading repository instructions file (repo.md)')
init_repo = False
else:
print_formatted_text(
'\nRepository instructions file will be created by exploring the repository.\n'
)
init_repo = cli_confirm(
'Do you want to proceed?',
['Yes, create', 'No, dismiss'],
)
return init_repo
def read_file(file_path):
with open(file_path, 'r') as f:
return f.read()
def write_to_file(file_path, content):
with open(file_path, 'w') as f:
f.write(content)
def cli_confirm(question: str = 'Are you sure?', choices: Optional[List[str]] = None):
if choices is None:
choices = ['Yes', 'No']
selected = [0] # Using list to allow modification in closure
def get_choice_text():
return [
('class:question', f'{question}\n\n'),
] + [
(
'class:selected' if i == selected[0] else 'class:unselected',
f"{'> ' if i == selected[0] else ' '}{choice}\n",
)
for i, choice in enumerate(choices)
]
kb = KeyBindings()
@kb.add('up')
def _(event):
selected[0] = (selected[0] - 1) % len(choices)
@kb.add('down')
def _(event):
selected[0] = (selected[0] + 1) % len(choices)
@kb.add('enter')
def _(event):
event.app.exit(result=selected[0] == 0)
style = Style.from_dict({'selected': COLOR_GOLD, 'unselected': ''})
layout = Layout(
HSplit(
[
Window(
FormattedTextControl(get_choice_text),
always_hide_cursor=True,
)
]
)
)
app = Application(
layout=layout,
key_bindings=kb,
style=style,
mouse_support=True,
full_screen=False,
)
return app.run(in_thread=True)
def update_usage_metrics(event: Event, usage_metrics: UsageMetrics):
"""Updates the UsageMetrics object with data from an event's llm_metrics."""
if hasattr(event, 'llm_metrics'):
llm_metrics: Metrics | None = getattr(event, 'llm_metrics', None)
if llm_metrics:
# Safely get accumulated_cost
cost = getattr(llm_metrics, 'accumulated_cost', 0)
# Ensure cost is a number before adding
usage_metrics.total_cost += cost if isinstance(cost, float) else 0
# Safely get token usage details object/dict
token_usage = getattr(llm_metrics, 'accumulated_token_usage', None)
if token_usage:
# Assume object access using getattr, providing defaults
prompt_tokens = getattr(token_usage, 'prompt_tokens', 0)
completion_tokens = getattr(token_usage, 'completion_tokens', 0)
cache_read = getattr(token_usage, 'cache_read_tokens', 0)
cache_write = getattr(token_usage, 'cache_write_tokens', 0)
# Ensure tokens are numbers before adding
usage_metrics.total_input_tokens += (
prompt_tokens if isinstance(prompt_tokens, int) else 0
)
usage_metrics.total_output_tokens += (
completion_tokens if isinstance(completion_tokens, int) else 0
)
usage_metrics.total_cache_read += (
cache_read if isinstance(cache_read, int) else 0
)
usage_metrics.total_cache_write += (
cache_write if isinstance(cache_write, int) else 0
)
def shutdown(usage_metrics: UsageMetrics, session_id: str):
cost_str = f'${usage_metrics.total_cost:.6f}'
input_tokens_str = f'{usage_metrics.total_input_tokens:,}'
cache_read_str = f'{usage_metrics.total_cache_read:,}'
cache_write_str = f'{usage_metrics.total_cache_write:,}'
output_tokens_str = f'{usage_metrics.total_output_tokens:,}'
total_tokens_str = (
f'{usage_metrics.total_input_tokens + usage_metrics.total_output_tokens:,}'
)
labels_and_values = [
(' Total Cost (USD):', cost_str),
(' Total Input Tokens:', input_tokens_str),
(' Cache Hits:', cache_read_str),
(' Cache Writes:', cache_write_str),
(' Total Output Tokens:', output_tokens_str),
(' Total Tokens:', total_tokens_str),
]
# Calculate max widths for alignment
max_label_width = max(len(label) for label, _ in labels_and_values)
max_value_width = max(len(value) for _, value in labels_and_values)
# Construct the summary text with aligned columns
summary_lines = [
f'{label:<{max_label_width}} {value:>{max_value_width}}'
for label, value in labels_and_values
]
summary_text = '\n'.join(summary_lines)
container = Frame(
TextArea(
text=summary_text,
read_only=True,
style=COLOR_GREY,
wrap_lines=True,
),
title='Session Summary',
style=f'fg:{COLOR_GREY}',
)
print_container(container)
print_formatted_text(HTML(f'\n<grey>Closed session {session_id}</grey>\n'))
def manage_openhands_file(folder_path=None, add_to_trusted=False):
openhands_file = Path.home() / '.openhands.toml'
default_content: dict = {'trusted_dirs': []}
if not openhands_file.exists():
with open(openhands_file, 'w') as f:
toml.dump(default_content, f)
if folder_path:
with open(openhands_file, 'r') as f:
try:
config = toml.load(f)
except Exception:
config = default_content
if 'trusted_dirs' not in config:
config['trusted_dirs'] = []
if folder_path in config['trusted_dirs']:
return True
if add_to_trusted:
config['trusted_dirs'].append(folder_path)
with open(openhands_file, 'w') as f:
toml.dump(config, f)
return False
return False
def check_folder_security_agreement(current_dir):
is_trusted = manage_openhands_file(current_dir)
if not is_trusted:
security_frame = Frame(
TextArea(
text=(
f'Do you trust the files in this folder?\n\n'
f'{current_dir}\n\n'
'OpenHands may read and execute files in this folder with your permission.'
),
style='#ffffff',
read_only=True,
wrap_lines=True,
)
)
clear()
print_container(security_frame)
confirm = cli_confirm('Do you wish to continue?', ['Yes, proceed', 'No, exit'])
if confirm:
manage_openhands_file(current_dir, add_to_trusted=True)
return confirm
return True
async def main(loop: asyncio.AbstractEventLoop):
"""Runs the agent in CLI mode."""
reload_microagents = False
args = parse_arguments()
logger.setLevel(logging.WARNING)
@@ -185,6 +649,12 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
# Load config from toml and override with command line arguments
config: AppConfig = setup_config_from_args(args)
# TODO: Set working directory from config or use current working directory?
current_dir = config.workspace_base
if not current_dir:
raise ValueError('Workspace base directory not specified')
# Read task from file, CLI args, or stdin
task_str = read_task(args, config.cli_multiline_input)
@@ -192,10 +662,19 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
initial_user_action = MessageAction(content=task_str) if task_str else None
sid = str(uuid4())
display_message(f'Session ID: {sid}')
is_loaded = asyncio.Event()
# Show OpenHands banner and session ID
display_banner(session_id=sid, is_loaded=is_loaded)
# Show Initialization loader
loop.run_in_executor(
None, display_initialization_animation, 'Initializing...', is_loaded
)
agent = create_agent(config)
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
agent.set_mcp_tools(mcp_tools)
runtime = create_runtime(
config,
sid=sid,
@@ -207,26 +686,64 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
event_stream = runtime.event_stream
async def prompt_for_next_task() -> None:
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
await prompt_for_next_task()
if next_message == 'exit':
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
usage_metrics = UsageMetrics()
async def prompt_for_next_task():
nonlocal reload_microagents
while True:
next_message = await read_prompt_input(config.cli_multiline_input)
if not next_message.strip():
continue
if next_message == '/exit':
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
shutdown(usage_metrics, sid)
return
elif next_message == '/help':
display_help()
continue
elif next_message == '/init':
if config.runtime == 'local':
init_repo = await init_repository(current_dir)
if init_repo:
event_stream.add_event(
MessageAction(content=REPO_MD_CREATE_PROMPT),
EventSource.USER,
)
reload_microagents = True
return
else:
print_formatted_text(
'\nRepository initialization through the CLI is only supported for local runtime.\n'
)
continue
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
return
action = MessageAction(content=next_message)
event_stream.add_event(action, EventSource.USER)
async def on_event_async(event: Event) -> None:
nonlocal reload_microagents
display_event(event, config)
update_usage_metrics(event, usage_metrics)
if isinstance(event, AgentStateChangedObservation):
if event.agent_state in [
AgentState.AWAITING_USER_INPUT,
AgentState.FINISHED,
]:
# Reload microagents after initialization of repo.md
if reload_microagents:
microagents: list[BaseMicroagent] = (
runtime.get_microagents_from_selected_repo(None)
)
memory.load_user_workspace_microagents(microagents)
reload_microagents = False
await prompt_for_next_task()
if event.agent_state == AgentState.AWAITING_USER_CONFIRMATION:
user_confirmed = await read_confirmation_input()
if user_confirmed:
@@ -264,6 +781,25 @@ async def main(loop: asyncio.AbstractEventLoop) -> None:
repo_directory=repo_directory,
)
# Clear loading animation
is_loaded.set()
if not check_folder_security_agreement(current_dir):
# User rejected, exit application
event_stream.add_event(
ChangeAgentStateAction(AgentState.STOPPED), EventSource.ENVIRONMENT
)
return
# Clear the terminal
clear()
# Show OpenHands banner and session ID
display_banner(session_id=sid, is_loaded=is_loaded)
# Show OpenHands welcome
display_welcome_message()
if initial_user_action:
# If there's an initial user action, enqueue it and do not prompt again
event_stream.add_event(initial_user_action, EventSource.USER)

View File

@@ -10,10 +10,9 @@ class AgentConfig(BaseModel):
"""Configuration for the agent.
Attributes:
function_calling: Whether function calling is enabled. Default is True.
codeact_enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
codeact_enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
enable_browsing: Whether browsing delegate is enabled in the action space. Default is False. Only works with function calling.
enable_llm_editor: Whether LLM editor is enabled in the action space. Default is False. Only works with function calling.
enable_jupyter: Whether Jupyter is enabled in the action space. Default is False.
llm_config: The name of the llm config to use. If specified, this will override global llm config.
enable_prompt_extensions: Whether to use prompt extensions (e.g., microagents, inject runtime info). Default is True.
disabled_microagents: A list of microagents to disable (by name, without .py extension, e.g. ["github", "lint"]). Default is None.
@@ -23,9 +22,9 @@ class AgentConfig(BaseModel):
"""
llm_config: str | None = Field(default=None)
codeact_enable_browsing: bool = Field(default=True)
codeact_enable_llm_editor: bool = Field(default=False)
codeact_enable_jupyter: bool = Field(default=True)
enable_browsing: bool = Field(default=True)
enable_llm_editor: bool = Field(default=False)
enable_jupyter: bool = Field(default=True)
enable_prompt_extensions: bool = Field(default=True)
disabled_microagents: list[str] = Field(default_factory=list)
enable_history_truncation: bool = Field(default=True)

View File

@@ -11,6 +11,7 @@ from openhands.core.config.config_utils import (
)
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
@@ -47,6 +48,7 @@ class AppConfig(BaseModel):
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
input is read line by line. When enabled, input continues until /exit command.
mcp: MCP configuration settings.
"""
llms: dict[str, LLMConfig] = Field(default_factory=dict)
@@ -88,6 +90,7 @@ class AppConfig(BaseModel):
max_concurrent_conversations: int = Field(
default=3
) # Maximum number of concurrent agent loops allowed per user
mcp: MCPConfig = Field(default_factory=MCPConfig)
defaults_dict: ClassVar[dict] = {}

View File

@@ -0,0 +1,68 @@
from typing import List
from urllib.parse import urlparse
from pydantic import BaseModel, Field, ValidationError
class MCPSSEConfig(BaseModel):
"""Configuration for MCP SSE (Server-Sent Events) settings.
Attributes:
mcp_servers: List of MCP server URLs.
"""
mcp_servers: List[str] = Field(default_factory=list)
model_config = {'extra': 'forbid'}
def validate_servers(self) -> None:
"""Validate that server URLs are valid and unique."""
# Check for duplicate server URLs
if len(set(self.mcp_servers)) != len(self.mcp_servers):
raise ValueError('Duplicate MCP server URLs are not allowed')
# Validate URLs
for url in self.mcp_servers:
try:
result = urlparse(url)
if not all([result.scheme, result.netloc]):
raise ValueError(f'Invalid URL format: {url}')
except Exception as e:
raise ValueError(f'Invalid URL {url}: {str(e)}')
class MCPConfig(BaseModel):
"""Configuration for MCP (Message Control Protocol) settings.
Attributes:
sse: SSE-specific configuration.
"""
sse: MCPSSEConfig = Field(default_factory=MCPSSEConfig)
model_config = {'extra': 'forbid'}
@classmethod
def from_toml_section(cls, data: dict) -> dict[str, 'MCPConfig']:
"""
Create a mapping of MCPConfig instances from a toml dictionary representing the [mcp] section.
The configuration is built from all keys in data.
Returns:
dict[str, MCPConfig]: A mapping where the key "mcp" corresponds to the [mcp] configuration
"""
# Initialize the result mapping
mcp_mapping: dict[str, MCPConfig] = {}
try:
# Create SSE config if present
sse_config = MCPSSEConfig.model_validate(data)
sse_config.validate_servers()
# Create the main MCP config
mcp_mapping['mcp'] = cls(sse=sse_config)
except ValidationError as e:
raise ValueError(f'Invalid MCP configuration: {e}')
return mcp_mapping

View File

@@ -23,6 +23,7 @@ from openhands.core.config.config_utils import (
)
from openhands.core.config.extended_config import ExtendedConfig
from openhands.core.config.llm_config import LLMConfig
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.config.sandbox_config import SandboxConfig
from openhands.core.config.security_config import SecurityConfig
from openhands.storage import get_file_store
@@ -202,6 +203,21 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
# Re-raise ValueError from SandboxConfig.from_toml_section
raise ValueError('Error in [sandbox] section in config.toml')
# Process MCP sections if present
if 'mcp' in toml_config:
try:
mcp_mapping = MCPConfig.from_toml_section(toml_config['mcp'])
# We only use the base mcp config for now
if 'mcp' in mcp_mapping:
cfg.mcp = mcp_mapping['mcp']
except (TypeError, KeyError, ValidationError) as e:
logger.openhands_logger.warning(
f'Cannot parse MCP config from toml, values have not been applied.\nError: {e}'
)
except ValueError:
# Re-raise ValueError from MCPConfig.from_toml_section
raise ValueError('Error in MCP sections in config.toml')
# Process condenser section if present
if 'condenser' in toml_config:
try:
@@ -259,6 +275,7 @@ def load_from_toml(cfg: AppConfig, toml_file: str = 'config.toml') -> None:
'security',
'sandbox',
'condenser',
'mcp',
}
for key in toml_config:
if key.lower() not in known_sections:

View File

@@ -222,14 +222,14 @@ class BrowserUnavailableException(Exception):
# ============================================
class MicroAgentError(Exception):
class MicroagentError(Exception):
"""Base exception for all microagent errors."""
pass
class MicroAgentValidationError(MicroAgentError):
class MicroagentValidationError(MicroagentError):
"""Raised when there's a validation error in microagent metadata."""
def __init__(self, message: str = 'Micro agent validation failed') -> None:
def __init__(self, message: str = 'Microagent validation failed') -> None:
super().__init__(message)

View File

@@ -30,6 +30,7 @@ from openhands.events.action.action import Action
from openhands.events.event import Event
from openhands.events.observation import AgentStateChangedObservation
from openhands.io import read_input, read_task
from openhands.mcp import fetch_mcp_tools_from_config
from openhands.memory.memory import Memory
from openhands.runtime.base import Runtime
from openhands.utils.async_utils import call_async_from_sync
@@ -95,6 +96,8 @@ async def run_controller(
if agent is None:
agent = create_agent(config)
mcp_tools = await fetch_mcp_tools_from_config(config.mcp)
agent.set_mcp_tools(mcp_tools)
# when the runtime is created, it will be connected and clone the selected repository
repo_directory = None

View File

@@ -38,6 +38,10 @@ class ActionType(str, Enum):
"""Interact with the browser instance.
"""
MCP = 'call_tool_mcp'
"""Interact with the MCP server.
"""
DELEGATE = 'delegate'
"""Delegates a task to another agent.
"""

View File

@@ -49,3 +49,6 @@ class ObservationType(str, Enum):
RECALL = 'recall'
"""Result of a recall operation. This can be the workspace context, a microagent, or other types of information."""
MCP = 'mcp'
"""Result of a MCP Server operation"""

View File

@@ -18,7 +18,7 @@ from openhands.events.event import Event
from openhands.integrations.provider import ProviderToken, ProviderType, SecretStore
from openhands.llm.llm import LLM
from openhands.memory.memory import Memory
from openhands.microagent.microagent import BaseMicroAgent
from openhands.microagent.microagent import BaseMicroagent
from openhands.runtime import get_runtime_cls
from openhands.runtime.base import Runtime
from openhands.security import SecurityAnalyzer, options
@@ -160,7 +160,7 @@ def create_memory(
memory.set_runtime_info(runtime)
# loads microagents from repo/.openhands/microagents
microagents: list[BaseMicroAgent] = runtime.get_microagents_from_selected_repo(
microagents: list[BaseMicroagent] = runtime.get_microagents_from_selected_repo(
selected_repository
)
memory.load_user_workspace_microagents(microagents)
@@ -175,6 +175,7 @@ def create_agent(config: AppConfig) -> Agent:
agent_cls: Type[Agent] = Agent.get_cls(config.default_agent)
agent_config = config.get_agent_config(config.default_agent)
llm_config = config.get_llm_config_from_agent(config.default_agent)
agent = agent_cls(
llm=LLM(config=llm_config),
config=agent_config,

View File

@@ -15,6 +15,7 @@ from openhands.events.action.files import (
FileReadAction,
FileWriteAction,
)
from openhands.events.action.mcp import McpAction
from openhands.events.action.message import MessageAction
__all__ = [
@@ -35,4 +36,5 @@ __all__ = [
'ActionConfirmationStatus',
'AgentThinkAction',
'RecallAction',
'McpAction',
]

View File

@@ -0,0 +1,32 @@
from dataclasses import dataclass
from typing import ClassVar
from openhands.core.schema import ActionType
from openhands.events.action.action import Action, ActionSecurityRisk
@dataclass
class McpAction(Action):
name: str
arguments: str | None = None
thought: str = ''
action: str = ActionType.MCP
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk | None = None
@property
def message(self) -> str:
return (
f'I am interacting with the MCP server with name:\n'
f'```\n{self.name}\n```\n'
f'and arguments:\n'
f'```\n{self.arguments}\n```'
)
def __str__(self) -> str:
ret = '**McpAction**\n'
if self.thought:
ret += f'THOUGHT: {self.thought}\n'
ret += f'NAME: {self.name}\n'
ret += f'ARGUMENTS: {self.arguments}'
return ret

View File

@@ -44,4 +44,5 @@ __all__ = [
'AgentCondensationObservation',
'RecallObservation',
'RecallType',
'MCPObservation',
]

View File

@@ -0,0 +1,15 @@
from dataclasses import dataclass
from openhands.core.schema import ObservationType
from openhands.events.observation.observation import Observation
@dataclass
class MCPObservation(Observation):
"""This data class represents the result of a MCP Server operation."""
observation: str = ObservationType.MCP
@property
def message(self) -> str:
return self.content

View File

@@ -22,6 +22,7 @@ from openhands.events.action.files import (
FileReadAction,
FileWriteAction,
)
from openhands.events.action.mcp import McpAction
from openhands.events.action.message import MessageAction
actions = (
@@ -41,6 +42,7 @@ actions = (
ChangeAgentStateAction,
MessageAction,
CondensationAction,
McpAction,
)
ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]

View File

@@ -5,6 +5,7 @@ from typing import Any
from pydantic import BaseModel
from openhands.core.logger import openhands_logger as logger
from openhands.events import Event, EventSource
from openhands.events.serialization.action import action_from_dict
from openhands.events.serialization.observation import observation_from_dict
@@ -134,11 +135,12 @@ def event_to_dict(event: 'Event') -> dict:
k: (v.value if isinstance(v, Enum) else _convert_pydantic_to_dict(v))
for k, v in props.items()
}
logger.debug(f'extras data in event_to_dict: {d["extras"]}')
# Include success field for CmdOutputObservation
if hasattr(event, 'success'):
d['success'] = event.success
else:
raise ValueError('Event must be either action or observation')
raise ValueError(f'Event must be either action or observation. has: {event}')
return d

View File

@@ -25,6 +25,7 @@ from openhands.events.observation.files import (
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
@@ -45,6 +46,7 @@ observations = (
AgentCondensationObservation,
AgentThinkObservation,
RecallObservation,
MCPObservation,
)
OBSERVATION_TYPE_TO_CLASS = {

View File

@@ -160,29 +160,38 @@ class EventStream(EventStore):
raise ValueError(
f'Event already has an ID:{event.id}. It was probably added back to the EventStream from inside a handler, triggering a loop.'
)
event._timestamp = datetime.now().isoformat()
event._source = source # type: ignore [attr-defined]
with self._lock:
event._id = self.cur_id # type: ignore [attr-defined]
self.cur_id += 1
logger.debug(f'Adding {type(event).__name__} id={event.id} from {source.name}')
event._timestamp = datetime.now().isoformat()
event._source = source # type: ignore [attr-defined]
data = event_to_dict(event)
data = self._replace_secrets(data)
event = event_from_dict(data)
# Take a copy of the current write page
current_write_page = self._write_page_cache
data = event_to_dict(event)
data = self._replace_secrets(data)
event = event_from_dict(data)
current_write_page.append(data)
# If the page is full, create a new page for future events / other threads to use
if len(current_write_page) == self.cache_size:
self._write_page_cache = []
if event.id is not None:
# Write the event to the store - this can take some time
self.file_store.write(
self._get_filename_for_id(event.id, self.user_id), json.dumps(data)
)
self._write_page_cache.append(data)
self._store_cache_page()
# Store the cache page last - if it is not present during reads then it will simply be bypassed.
self._store_cache_page(current_write_page)
self._queue.put(event)
def _store_cache_page(self):
def _store_cache_page(self, current_write_page: list[dict]):
"""Store a page in the cache. Reading individual events is slow when there are a lot of them, so we use pages."""
current_write_page = self._write_page_cache
if len(current_write_page) < self.cache_size:
return
self._write_page_cache = []
start = current_write_page[0]['id']
end = start + self.cache_size
contents = json.dumps(current_write_page)

View File

@@ -36,7 +36,15 @@ def dumps(obj, **kwargs):
"""Serialize an object to str format"""
if not kwargs:
return _json_encoder.encode(obj)
return json.dumps(obj, cls=OpenHandsJSONEncoder, **kwargs)
# Create a copy of the kwargs to avoid modifying the original
encoder_kwargs = kwargs.copy()
# If cls is specified, use it; otherwise use our custom encoder
if 'cls' not in encoder_kwargs:
encoder_kwargs['cls'] = OpenHandsJSONEncoder
return json.dumps(obj, **encoder_kwargs)
def loads(json_str, **kwargs):

View File

@@ -375,12 +375,17 @@ class LLM(RetryMixin, DebugMixin):
if self.config.model.startswith('litellm_proxy/'):
# IF we are using LiteLLM proxy, get model info from LiteLLM proxy
# GET {base_url}/v1/model/info with litellm_model_id as path param
base_url = self.config.base_url.strip() if self.config.base_url else ''
if not base_url.startswith(('http://', 'https://')):
base_url = 'http://' + base_url
response = httpx.get(
f'{self.config.base_url}/v1/model/info',
f'{base_url}/v1/model/info',
headers={
'Authorization': f'Bearer {self.config.api_key.get_secret_value() if self.config.api_key else None}'
},
)
resp_json = response.json()
if 'data' not in resp_json:
logger.error(

21
openhands/mcp/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
from openhands.mcp.client import MCPClient
from openhands.mcp.tool import (
BaseTool,
MCPClientTool,
)
from openhands.mcp.utils import (
call_tool_mcp,
convert_mcp_clients_to_tools,
create_mcp_clients,
fetch_mcp_tools_from_config,
)
__all__ = [
'MCPClient',
'convert_mcp_clients_to_tools',
'create_mcp_clients',
'BaseTool',
'MCPClientTool',
'fetch_mcp_tools_from_config',
'call_tool_mcp',
]

98
openhands/mcp/client.py Normal file
View File

@@ -0,0 +1,98 @@
from contextlib import AsyncExitStack
from typing import Dict, List, Optional
from mcp import ClientSession
from mcp.client.sse import sse_client
from pydantic import BaseModel, Field
from openhands.core.logger import openhands_logger as logger
from openhands.mcp.tool import BaseTool, MCPClientTool
class MCPClient(BaseModel):
"""
A collection of tools that connects to an MCP server and manages available tools through the Model Context Protocol.
"""
session: Optional[ClientSession] = None
exit_stack: AsyncExitStack = AsyncExitStack()
description: str = 'MCP client tools for server interaction'
tools: List[BaseTool] = Field(default_factory=list)
tool_map: Dict[str, BaseTool] = Field(default_factory=dict)
class Config:
arbitrary_types_allowed = True
async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None:
"""Connect to an MCP server using SSE transport.
Args:
server_url: The URL of the SSE server to connect to.
timeout: Connection timeout in seconds. Default is 30 seconds.
"""
if not server_url:
raise ValueError('Server URL is required.')
if self.session:
await self.disconnect()
try:
streams_context = sse_client(
url=server_url,
)
streams = await self.exit_stack.enter_async_context(streams_context)
self.session = await self.exit_stack.enter_async_context(
ClientSession(*streams)
)
await self._initialize_and_list_tools()
except Exception as e:
logger.error(f'Error connecting to {server_url}: {str(e)}')
raise
async def _initialize_and_list_tools(self) -> None:
"""Initialize session and populate tool map."""
if not self.session:
raise RuntimeError('Session not initialized.')
await self.session.initialize()
response = await self.session.list_tools()
# Clear existing tools
self.tools = []
# Create proper tool objects for each server tool
for tool in response.tools:
server_tool = MCPClientTool(
name=tool.name,
description=tool.description,
inputSchema=tool.inputSchema,
session=self.session,
)
self.tool_map[tool.name] = server_tool
self.tools.append(server_tool)
logger.info(
f'Connected to server with tools: {[tool.name for tool in response.tools]}'
)
async def call_tool(self, tool_name: str, args: Dict):
"""Call a tool on the MCP server."""
if tool_name not in self.tool_map:
raise ValueError(f'Tool {tool_name} not found.')
return await self.tool_map[tool_name].execute(**args)
async def disconnect(self) -> None:
"""Disconnect from the MCP server and clean up resources."""
if self.session:
try:
# Close the session first
if hasattr(self.session, 'close'):
await self.session.close()
# Then close the exit stack
await self.exit_stack.aclose()
except Exception as e:
logger.error(f'Error during disconnect: {str(e)}')
finally:
self.session = None
self.tools = []
logger.info('Disconnected from MCP server')

54
openhands/mcp/tool.py Normal file
View File

@@ -0,0 +1,54 @@
from abc import ABC, abstractmethod
from typing import Dict, Optional
from mcp import ClientSession
from mcp.types import CallToolResult, TextContent, Tool
class BaseTool(ABC, Tool):
@classmethod
def postfix(cls) -> str:
return '_mcp_tool_call'
class Config:
arbitrary_types_allowed = True
@abstractmethod
async def execute(self, **kwargs) -> CallToolResult:
"""Execute the tool with given parameters."""
def to_param(self) -> Dict:
"""Convert tool to function call format."""
return {
'type': 'function',
'function': {
'name': self.name + self.postfix(),
'description': self.description,
'parameters': self.inputSchema,
},
}
class MCPClientTool(BaseTool):
"""Represents a tool proxy that can be called on the MCP server from the client side."""
session: Optional[ClientSession] = None
async def execute(self, **kwargs) -> CallToolResult:
"""Execute the tool by making a remote call to the MCP server."""
if not self.session:
return CallToolResult(
content=[TextContent(text='Not connected to MCP server', type='text')],
isError=True,
)
try:
result = await self.session.call_tool(self.name, kwargs)
return result
except Exception as e:
return CallToolResult(
content=[
TextContent(text=f'Error executing tool: {str(e)}', type='text')
],
isError=True,
)

135
openhands/mcp/utils.py Normal file
View File

@@ -0,0 +1,135 @@
import json
from openhands.core.config.mcp_config import MCPConfig
from openhands.core.logger import openhands_logger as logger
from openhands.events.action.mcp import McpAction
from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.mcp.client import MCPClient
def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[dict]:
"""
Converts a list of MCPClient instances to ChatCompletionToolParam format
that can be used by CodeActAgent.
Args:
mcp_clients: List of MCPClient instances or None
Returns:
List of dicts of tools ready to be used by CodeActAgent
"""
if mcp_clients is None:
logger.warning('mcp_clients is None, returning empty list')
return []
all_mcp_tools = []
try:
for client in mcp_clients:
# Each MCPClient has an mcp_clients property that is a ToolCollection
# The ToolCollection has a to_params method that converts tools to ChatCompletionToolParam format
for tool in client.tools:
mcp_tools = tool.to_param()
all_mcp_tools.append(mcp_tools)
except Exception as e:
logger.error(f'Error in convert_mcp_clients_to_tools: {e}')
return []
return all_mcp_tools
async def create_mcp_clients(
sse_mcp_server: list[str],
) -> list[MCPClient]:
mcp_clients: list[MCPClient] = []
# Initialize SSE connections
if sse_mcp_server:
for server_url in sse_mcp_server:
logger.info(
f'Initializing MCP agent for {server_url} with SSE connection...'
)
client = MCPClient()
try:
await client.connect_sse(server_url)
# Only add the client to the list after a successful connection
mcp_clients.append(client)
logger.info(f'Connected to MCP server {server_url} via SSE')
except Exception as e:
logger.error(f'Failed to connect to {server_url}: {str(e)}')
try:
await client.disconnect()
except Exception as disconnect_error:
logger.error(
f'Error during disconnect after failed connection: {str(disconnect_error)}'
)
return mcp_clients
async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]:
"""
Retrieves the list of MCP tools from the MCP clients.
Returns:
A list of tool dictionaries. Returns an empty list if no connections could be established.
"""
mcp_clients = []
mcp_tools = []
try:
logger.debug(f'Creating MCP clients with config: {mcp_config}')
mcp_clients = await create_mcp_clients(
mcp_config.sse.mcp_servers,
)
if not mcp_clients:
logger.warning('No MCP clients were successfully connected')
return []
mcp_tools = convert_mcp_clients_to_tools(mcp_clients)
# Always disconnect clients to clean up resources
for mcp_client in mcp_clients:
try:
await mcp_client.disconnect()
except Exception as disconnect_error:
logger.error(f'Error disconnecting MCP client: {str(disconnect_error)}')
except Exception as e:
logger.error(f'Error fetching MCP tools: {str(e)}')
return []
logger.debug(f'MCP tools: {mcp_tools}')
return mcp_tools
async def call_tool_mcp(mcp_clients: list[MCPClient], action: McpAction) -> Observation:
"""
Call a tool on an MCP server and return the observation.
Args:
action: The MCP action to execute
sse_mcp_servers: List of SSE MCP server URLs
Returns:
The observation from the MCP server
"""
if not mcp_clients:
raise ValueError('No MCP clients found')
logger.debug(f'MCP action received: {action}')
# Find the MCP agent that has the matching tool name
matching_client = None
logger.debug(f'MCP clients: {mcp_clients}')
logger.debug(f'MCP action name: {action.name}')
for client in mcp_clients:
logger.debug(f'MCP client tools: {client.tools}')
if action.name in [tool.name for tool in client.tools]:
matching_client = client
break
if matching_client is None:
raise ValueError(f'No matching MCP agent found for tool name: {action.name}')
logger.debug(f'Matching client: {matching_client}')
args_dict = json.loads(action.arguments) if action.arguments else {}
response = await matching_client.call_tool(action.name, args_dict)
logger.debug(f'MCP response: {response}')
return MCPObservation(content=f'MCP result:{response.model_dump(mode="json")}')

View File

@@ -114,8 +114,14 @@ class LLMAttentionCondenser(RollingCondenser):
@classmethod
def from_config(cls, config: LLMAttentionCondenserConfig) -> LLMAttentionCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return LLMAttentionCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
)

View File

@@ -155,8 +155,14 @@ CURRENT_STATE: Last flip: Heads, Haiku count: 15/20"""
def from_config(
cls, config: LLMSummarizingCondenserConfig
) -> LLMSummarizingCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return LLMSummarizingCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,

View File

@@ -311,8 +311,14 @@ Capture all relevant information, especially:
def from_config(
cls, config: StructuredSummaryCondenserConfig
) -> StructuredSummaryCondenser:
# This condenser cannot take advantage of prompt caching. If it happens
# to be set, we'll pay for the cache writes but never get a chance to
# save on a read.
llm_config = config.llm_config.model_copy()
llm_config.caching_prompt = False
return StructuredSummaryCondenser(
llm=LLM(config=config.llm_config),
llm=LLM(config=llm_config),
max_size=config.max_size,
keep_first=config.keep_first,
max_event_length=config.max_event_length,

View File

@@ -19,6 +19,7 @@ from openhands.events.action import (
IPythonRunCellAction,
MessageAction,
)
from openhands.events.action.mcp import McpAction
from openhands.events.event import Event, RecallType
from openhands.events.observation import (
AgentCondensationObservation,
@@ -36,6 +37,7 @@ from openhands.events.observation.agent import (
RecallObservation,
)
from openhands.events.observation.error import ErrorObservation
from openhands.events.observation.mcp import MCPObservation
from openhands.events.observation.observation import Observation
from openhands.events.serialization.event import truncate_content
from openhands.utils.prompt import PromptManager, RepositoryInfo, RuntimeInfo
@@ -167,7 +169,7 @@ class ConversationMemory:
- BrowseInteractiveAction: For browsing the web
- AgentFinishAction: For ending the interaction
- MessageAction: For sending messages
- McpAction: For interacting with the MCP server
pending_tool_call_action_messages: Dictionary mapping response IDs to their corresponding messages.
Used in function calling mode to track tool calls that are waiting for their results.
@@ -193,6 +195,7 @@ class ConversationMemory:
FileReadAction,
BrowseInteractiveAction,
BrowseURLAction,
McpAction,
),
) or (isinstance(action, CmdRunAction) and action.source == 'agent'):
tool_metadata = action.tool_call_metadata
@@ -326,6 +329,10 @@ class ConversationMemory:
else:
text = truncate_content(obs.to_agent_observation(), max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, MCPObservation):
# logger.warning(f'MCPObservation: {obs}')
text = truncate_content(obs.content, max_message_chars)
message = Message(role='user', content=[TextContent(text=text)])
elif isinstance(obs, IPythonRunCellObservation):
text = obs.content
# replace base64 images with a placeholder

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