Compare commits

...

38 Commits

Author SHA1 Message Date
openhands 6c250b0c39 Fix: Explicitly instruct agent to respect pre-commit hooks
This commit addresses issue #11409 where pre-commit hooks were not being
automatically executed when the agent creates commits for pull requests.

Changes made:
1. Updated microagents (github.md, gitlab.md, bitbucket.md) to explicitly
   instruct agents to NEVER use the --no-verify flag when making commits,
   ensuring pre-commit hooks configured in .openhands/pre-commit.sh are
   always executed.

2. Updated system_prompt.j2 for CodeActAgent to include explicit instruction
   against using --no-verify in the VERSION_CONTROL section.

3. Added comprehensive tests in test_pre_commit_instructions.py to verify
   that all git-related microagents and the system prompt contain proper
   instructions about respecting pre-commit hooks.

The fix ensures that:
- Pre-commit hooks are executed to enforce code quality standards
- Project-specific checks configured by users are always respected
- The agent's behavior aligns with standard git workflows

Fixes #11409

Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-16 17:20:18 +00:00
olyashok 5219f85bfa feat: make websocket client wait timeout configurable (#11405)
Co-authored-by: Alex <alex@cellect.ai>
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-16 16:49:50 +00:00
Kevin Musgrave a237b578c0 feat(evaluation): Add multi-swe-bench dependency and fix rollout script (#11326)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-16 14:35:19 +00:00
mogith-pn f42a4f75cb feat: Clarifai Integration as LLM Provider (#11324) 2025-10-16 18:23:00 +04:00
Engel Nyst 3e645f8649 fix(integration-tests): accept --eval-num-workers and --eval-note in integration test runner (#11387) 2025-10-16 09:50:24 -04:00
Ryan H. Tran 5182388323 Extend context truncation cases (#11393) 2025-10-16 17:55:57 +07:00
juanmichelini 471d272c7c Mint security eval fix (#11273)
Co-authored-by: Xingyao Wang <xingyao@all-hands.dev>
2025-10-16 01:42:05 +00:00
Tim O'Farrell 0522734875 Add ProcessSandboxService implementation for process-based sandboxes (#11394)
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-15 17:53:50 -06:00
Tim O'Farrell f4fd8ea907 Added flag to disable the V1 endpoints inside nested V0 runtimes (#11391) 2025-10-15 15:33:52 -06:00
Engel Nyst e9413aaded Update header logo branding to OpenHands (#11383)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-15 21:28:22 +02:00
sp.wack ef004962cc hotfix(backend): Update route parameters from 'id' to 'sandbox_id' (#11389) 2025-10-15 16:40:10 +00:00
Hiep Le 58d67a2480 fix(backend): repository search is not working in the production environment (#11386) 2025-10-15 23:24:27 +07:00
Tim O'Farrell 72179f45d3 Fir for broken V1 db connection (#11382) 2025-10-15 08:07:43 -04:00
Ray Myers 15e7709ff6 chore - Add README notice of coming org rename (#11381) 2025-10-14 23:39:12 -05:00
Christopher Pereira bb563d6dd1 Fix typos (#11162) 2025-10-14 14:01:51 -04:00
Hiep Le d991b9880d fix(frontend): reo tracker should be available only in the SaaS environment, not in self-hosted instances (#11367) 2025-10-14 22:16:45 +07:00
Rohit Malhotra fe82cfd277 Hotfix(CLI VI): unable to launch via default entrypoint (#11354) 2025-10-14 10:39:49 -04:00
Cesar Garcia 16fa8ea7be Fix broken logo in README.md (#11366) 2025-10-14 14:29:27 +00:00
Tim O'Farrell f292f3a84d V1 Integration (#11183)
Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: sp.wack <83104063+amanape@users.noreply.github.com>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-10-14 02:16:44 +00:00
Rohit Malhotra 5076f21e86 CLI(V1): Patch release (#11349) 2025-10-13 22:11:59 +00:00
Rohit Malhotra 2640d43159 Fix API key disappearing bug when updating CLI settings (#11351)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-13 21:02:58 +00:00
Rohit Malhotra 609fefc1b6 Fix CLI binary GLIBC compatibility for older Linux systems (#11337)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-13 18:52:52 +00:00
Rohit Malhotra 5db0d495d4 RM CLI version on opening page (#11347) 2025-10-13 18:33:57 +00:00
Rohit Malhotra 60fa7b3d01 [Hotfix, CLI(V1)]: Prevent crashing cli when confirmation mode disabled (#11343) 2025-10-13 17:43:22 +00:00
Rohit Malhotra cca2a55166 Fix openhands CLI executable entry point in pyproject.toml (#11338)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-13 15:04:46 +00:00
Ryan H. Tran c5e58572d5 fix(cli): escape action content before passing to HTML (#11333) 2025-10-13 22:02:26 +07:00
Alona baaa41ed99 feat: Add Bitbucket Resolver templates (#10880) 2025-10-13 10:23:24 -04:00
Kevin Musgrave 19bae5ac0f feat(evaluation): Add placeholders to swe_gpt4.j2 (#11228)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-10-13 22:15:05 +08:00
rstar327 93e1cd44c6 fix: (frontend) clean up unsed error variable in try/catch block (#11325) 2025-10-13 14:13:11 +00:00
llamantino c0ce78c64a fix: remove the hardcoded 5-minute timeout from the docker pull command (#11322) 2025-10-13 10:00:10 -04:00
Bogdan Petković 399bf92ed1 Fix: Correct rename detection in apply_patch to check per-diff instead of full patch (#10913)
Signed-off-by: Bogdan Petkovic <bogdan@fatdragon.dev>
Co-authored-by: Bogdan Petkovic <bogdan@fatdragon.dev>
2025-10-13 09:47:01 -04:00
Ray Myers 2bbe15a329 chore - CI check migrations are in sync and warn (#10946) 2025-10-10 15:19:00 -05:00
mamoodi 6f22092d07 Release 0.59.0 (#11319) 2025-10-10 15:31:38 -04:00
Rohit Malhotra c034cc5dfb Refactor: move helper function to avoid circular imports (#11310)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 12:40:03 -04:00
Hiep Le 9bd02440b0 fix(frontend): some user interface elements are overlapping with the Create API Key modal (#11301) 2025-10-10 22:54:10 +07:00
Rohit Malhotra c9d8782566 V1(CLI): Release (#11317) 2025-10-10 15:25:19 +00:00
sp.wack ef49994700 feat(frontend): V1 WebSocket handler (#11221)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 16:29:27 +04:00
Rohit Malhotra 0f92bdc9a8 V1(CLI): Use built-in token for release (#11307)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-10-10 04:27:11 +00:00
229 changed files with 16513 additions and 568 deletions
@@ -1,5 +1,5 @@
# Workflow that builds and tests the CLI binary executable
name: CLI - Build and Test Binary
name: CLI - Build binary and optionally release
# Run on pushes to main branch and CLI tags, and on pull requests when CLI files change
on:
@@ -12,17 +12,28 @@ on:
paths:
- "openhands-cli/**"
permissions:
contents: write # needed to create releases or upload assets
# Cancel previous runs if a new commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ (github.head_ref && github.ref) || github.run_id }}
cancel-in-progress: true
jobs:
build-and-test-binary:
name: Build and test binary executable
build-binary:
name: Build binary executable
strategy:
matrix:
os: [ubuntu-latest, macos-latest]
include:
# Build on Ubuntu 22.04 for maximum GLIBC compatibility (GLIBC 2.31)
- os: ubuntu-22.04
platform: linux
artifact_name: openhands-cli-linux
# Build on macOS for macOS users
- os: macos-15
platform: macos
artifact_name: openhands-cli-macos
runs-on: ${{ matrix.os }}
steps:
@@ -60,18 +71,17 @@ jobs:
echo "✅ Build & test finished without ❌ markers"
- name: Upload binary artifact (for releases only)
if: startsWith(github.ref, 'refs/tags/')
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: openhands-cli-${{ matrix.os }}
name: ${{ matrix.artifact_name }}
path: openhands-cli/dist/openhands*
retention-days: 30
create-github-release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: build-and-test-binary
needs: build-binary
if: startsWith(github.ref, 'refs/tags/')
steps:
- name: Checkout repository
@@ -85,12 +95,12 @@ jobs:
- name: Prepare release assets
run: |
mkdir -p release-assets
# Rename binaries to include OS in filename
if [ -f artifacts/openhands-cli-ubuntu-latest/openhands ]; then
cp artifacts/openhands-cli-ubuntu-latest/openhands release-assets/openhands-linux
# Copy binaries with appropriate names for release
if [ -f artifacts/openhands-cli-linux/openhands ]; then
cp artifacts/openhands-cli-linux/openhands release-assets/openhands-linux
fi
if [ -f artifacts/openhands-cli-macos-latest/openhands ]; then
cp artifacts/openhands-cli-macos-latest/openhands release-assets/openhands-macos
if [ -f artifacts/openhands-cli-macos/openhands ]; then
cp artifacts/openhands-cli-macos/openhands release-assets/openhands-macos
fi
ls -la release-assets/
@@ -101,4 +111,4 @@ jobs:
draft: true
prerelease: false
env:
GITHUB_TOKEN: ${{ secrets.CLI_RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -0,0 +1,52 @@
name: Enterprise Check Migrations
on:
pull_request:
paths:
- 'enterprise/migrations/**'
jobs:
check-sync:
runs-on: ubuntu-latest
steps:
- name: Checkout PR branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.event.pull_request.base.ref }}
- name: Check if base branch is ancestor of PR
id: check_up_to_date
shell: bash
run: |
BASE="origin/${{ github.event.pull_request.base.ref }}"
HEAD="${{ github.event.pull_request.head.sha }}"
if git merge-base --is-ancestor "$BASE" "$HEAD"; then
echo "We're up to date with base $BASE"
exit 0
else
echo "NOT up to date with base $BASE"
exit 1
fi
- name: Find Comment
uses: peter-evans/find-comment@v3
id: find-comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: 'github-actions[bot]'
body-includes: |
⚠️ This PR contains **migrations**
- name: Comment warning on PR
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
comment-id: ${{ steps.find-comment.outputs.comment-id }}
edit-mode: replace
body: |
⚠️ This PR contains **migrations**. Please synchronize before merging to prevent conflicts.
+1 -1
View File
@@ -126,7 +126,7 @@ jobs:
- name: Install Python dependencies using Poetry
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
run: poetry run python3 -m openhands.runtime.utils.runtime_build --base_image ${{ matrix.base_image.image }} --build_folder containers/runtime --force_rebuild
- name: Lowercase Repository Owner
run: |
echo REPO_OWNER=$(echo ${{ github.repository_owner }} | tr '[:upper:]' '[:lower:]') >> $GITHUB_ENV
+1 -1
View File
@@ -71,7 +71,7 @@ jobs:
run: pip install pre-commit==4.2.0
- name: Run pre-commit hooks
working-directory: ./enterprise
run: pre-commit run --all-files --config ./dev_config/python/.pre-commit-config.yaml
run: pre-commit run --all-files --show-diff-on-failure --config ./dev_config/python/.pre-commit-config.yaml
lint-cli-python:
name: Lint CLI python
+15 -8
View File
@@ -1,14 +1,17 @@
# Publishes the OpenHands PyPi package
name: Publish PyPi Package
on:
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
description: "What are you publishing?"
required: true
default: ''
type: choice
options:
- app server
- cli
default: app server
push:
tags:
- "*"
@@ -16,8 +19,10 @@ on:
jobs:
release:
runs-on: blacksmith-4vcpu-ubuntu-2204
# Only run for tags that don't contain '-cli'
if: startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli')
# Run when manually dispatched for "app server" OR for tag pushes that don't contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'app server')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && !contains(github.ref, '-cli'))
steps:
- uses: actions/checkout@v4
- uses: useblacksmith/setup-python@v6
@@ -38,8 +43,10 @@ jobs:
release-cli:
name: Publish CLI to PyPI
runs-on: ubuntu-latest
# Only run for tags that contain '-cli'
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli')
# Run when manually dispatched for "cli" OR for tag pushes that contain '-cli'
if: |
(github.event_name == 'workflow_dispatch' && github.event.inputs.reason == 'cli')
|| (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') && contains(github.ref, '-cli'))
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -64,4 +71,4 @@ jobs:
- name: Publish CLI to PyPI
working-directory: openhands-cli
run: |
uv publish --token ${{ secrets.PYPI_TOKEN }}
uv publish --token ${{ secrets.PYPI_TOKEN_OPENHANDS }}
+1 -1
View File
@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.58-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.59-nikolaik`
## Develop inside Docker container
+10 -4
View File
@@ -1,7 +1,7 @@
<a name="readme-top"></a>
<div align="center">
<img src="./docs/static/img/logo.png" alt="Logo" width="200">
<img src="https://raw.githubusercontent.com/All-Hands-AI/docs/main/openhands/static/img/logo.png" alt="Logo" width="200">
<h1 align="center">OpenHands: Code Less, Make More</h1>
</div>
@@ -38,6 +38,12 @@ call APIs, and yes—even copy code snippets from StackOverflow.
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]
> **Upcoming change**: We are renaming our GitHub Org from `All-Hands-AI` to `OpenHands` on October 20th, 2025.
> Check the [tracking issue](https://github.com/All-Hands-AI/OpenHands/issues/11376) for more information.
> [!IMPORTANT]
> Using OpenHands for work? We'd love to chat! Fill out
> [this short form](https://docs.google.com/forms/d/e/1FAIpQLSet3VbGaz8z32gW9Wm-Grl4jpt5WgMXPgJ4EDPVmCETCBpJtQ/viewform)
@@ -76,17 +82,17 @@ You'll find OpenHands running at [http://localhost:3000](http://localhost:3000)
You can also run OpenHands directly with Docker:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.58
docker.all-hands.dev/all-hands-ai/openhands:0.59
```
</details>
+1 -1
View File
@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.58-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.59-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
+1 -1
View File
@@ -6,7 +6,7 @@ that depends on the `base_image` **AND** a [Python source distribution](https://
The following command will generate a `Dockerfile` file for `nikolaik/python-nodejs:python3.12-nodejs22` (the default base image), an updated `config.sh` and the runtime source distribution files/folders into `containers/runtime`:
```bash
poetry run python3 openhands/runtime/utils/runtime_build.py \
poetry run python3 -m openhands.runtime.utils.runtime_build \
--base_image nikolaik/python-nodejs:python3.12-nodejs22 \
--build_folder containers/runtime
```
+1 -1
View File
@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.58-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.59-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:
@@ -13,7 +13,7 @@ from integrations.solvability.models.report import SolvabilityReport
from integrations.solvability.models.summary import SolvabilitySummary
from integrations.utils import ENABLE_SOLVABILITY_ANALYSIS
from pydantic import ValidationError
from server.auth.token_manager import get_config
from server.config import get_config
from storage.database import session_maker
from storage.saas_settings_store import SaasSettingsStore
@@ -19,7 +19,8 @@ from integrations.utils import (
from jinja2 import Environment
from pydantic.dataclasses import dataclass
from server.auth.constants import GITHUB_APP_CLIENT_ID, GITHUB_APP_PRIVATE_KEY
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.proactive_conversation_store import ProactiveConversationStore
from storage.saas_secrets_store import SaasSecretsStore
@@ -4,7 +4,8 @@ from integrations.models import Message
from integrations.types import ResolverViewInterface, UserData
from integrations.utils import HOST, get_oh_labels, has_exact_mention
from jinja2 import Environment
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from storage.database import session_maker
from storage.saas_secrets_store import SaasSecretsStore
+5
View File
@@ -0,0 +1,5 @@
# Enterprise Migrations
## Migration conflicts
OpenHands PRs can fall out of sync with `main` quickly. When adding a migration, it's safest to sync the PR with main before merging to ensure you are caught up to any others that have been added.
@@ -0,0 +1,259 @@
"""Sync DB with Models
Revision ID: 076
Revises: 075
Create Date: 2025-10-05 11:28:41.772294
"""
from typing import Sequence, Union
import sqlalchemy as sa
from alembic import op
from openhands.app_server.app_conversation.app_conversation_models import (
AppConversationStartTaskStatus,
)
from openhands.app_server.event_callback.event_callback_result_models import (
EventCallbackResultStatus,
)
# revision identifiers, used by Alembic.
revision: str = '076'
down_revision: Union[str, Sequence[str], None] = '075'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
op.add_column(
'conversation_metadata',
sa.Column('max_budget_per_task', sa.Float(), nullable=True),
)
op.add_column(
'conversation_metadata',
sa.Column('cache_read_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('cache_write_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('reasoning_tokens', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('context_window', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column('per_turn_token', sa.Integer(), server_default='0'),
)
op.add_column(
'conversation_metadata',
sa.Column(
'conversation_version', sa.String(), nullable=False, server_default='V0'
),
)
op.create_index(
op.f('ix_conversation_metadata_conversation_version'),
'conversation_metadata',
['conversation_version'],
unique=False,
)
op.add_column('conversation_metadata', sa.Column('sandbox_id', sa.String()))
op.create_index(
op.f('ix_conversation_metadata_sandbox_id'),
'conversation_metadata',
['sandbox_id'],
unique=False,
)
op.create_table(
'app_conversation_start_task',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('created_by_user_id', sa.String(), nullable=True),
sa.Column('status', sa.Enum(AppConversationStartTaskStatus), nullable=True),
sa.Column('detail', sa.String(), nullable=True),
sa.Column('app_conversation_id', sa.UUID(), nullable=True),
sa.Column('sandbox_id', sa.String(), nullable=True),
sa.Column('agent_server_url', sa.String(), nullable=True),
sa.Column('request', sa.JSON(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_app_conversation_start_task_created_at'),
'app_conversation_start_task',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_app_conversation_start_task_created_by_user_id'),
'app_conversation_start_task',
['created_by_user_id'],
unique=False,
)
op.create_index(
op.f('ix_app_conversation_start_task_updated_at'),
'app_conversation_start_task',
['updated_at'],
unique=False,
)
op.create_table(
'event_callback',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('conversation_id', sa.UUID(), nullable=True),
sa.Column('processor', sa.JSON(), nullable=True),
sa.Column('event_kind', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_event_callback_created_at'),
'event_callback',
['created_at'],
unique=False,
)
op.create_table(
'event_callback_result',
sa.Column('id', sa.UUID(), nullable=False),
sa.Column('status', sa.Enum(EventCallbackResultStatus), nullable=True),
sa.Column('event_callback_id', sa.UUID(), nullable=True),
sa.Column('event_id', sa.UUID(), nullable=True),
sa.Column('conversation_id', sa.UUID(), nullable=True),
sa.Column('detail', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_event_callback_result_conversation_id'),
'event_callback_result',
['conversation_id'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_created_at'),
'event_callback_result',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_event_callback_id'),
'event_callback_result',
['event_callback_id'],
unique=False,
)
op.create_index(
op.f('ix_event_callback_result_event_id'),
'event_callback_result',
['event_id'],
unique=False,
)
op.create_table(
'v1_remote_sandbox',
sa.Column('id', sa.String(), nullable=False),
sa.Column('created_by_user_id', sa.String(), nullable=True),
sa.Column('sandbox_spec_id', sa.String(), nullable=True),
sa.Column(
'created_at',
sa.DateTime(timezone=True),
server_default=sa.text('(CURRENT_TIMESTAMP)'),
nullable=True,
),
sa.PrimaryKeyConstraint('id'),
)
op.create_index(
op.f('ix_v1_remote_sandbox_created_at'),
'v1_remote_sandbox',
['created_at'],
unique=False,
)
op.create_index(
op.f('ix_v1_remote_sandbox_created_by_user_id'),
'v1_remote_sandbox',
['created_by_user_id'],
unique=False,
)
op.create_index(
op.f('ix_v1_remote_sandbox_sandbox_spec_id'),
'v1_remote_sandbox',
['sandbox_spec_id'],
unique=False,
)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(
op.f('ix_v1_remote_sandbox_sandbox_spec_id'), table_name='v1_remote_sandbox'
)
op.drop_index(
op.f('ix_v1_remote_sandbox_created_by_user_id'), table_name='v1_remote_sandbox'
)
op.drop_index(
op.f('ix_v1_remote_sandbox_created_at'), table_name='v1_remote_sandbox'
)
op.drop_table('v1_remote_sandbox')
op.drop_index(
op.f('ix_event_callback_result_event_id'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_event_callback_id'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_created_at'),
table_name='event_callback_result',
)
op.drop_index(
op.f('ix_event_callback_result_conversation_id'),
table_name='event_callback_result',
)
op.drop_table('event_callback_result')
op.drop_index(op.f('ix_event_callback_created_at'), table_name='event_callback')
op.drop_table('event_callback')
op.drop_index(
op.f('ix_app_conversation_start_task_updated_at'),
table_name='app_conversation_start_task',
)
op.drop_index(
op.f('ix_app_conversation_start_task_created_by_user_id'),
table_name='app_conversation_start_task',
)
op.drop_index(
op.f('ix_app_conversation_start_task_created_at'),
table_name='app_conversation_start_task',
)
op.drop_table('app_conversation_start_task')
op.drop_column('conversation_metadata', 'sandbox_id')
op.drop_column('conversation_metadata', 'conversation_version')
op.drop_column('conversation_metadata', 'per_turn_token')
op.drop_column('conversation_metadata', 'context_window')
op.drop_column('conversation_metadata', 'reasoning_tokens')
op.drop_column('conversation_metadata', 'cache_write_tokens')
op.drop_column('conversation_metadata', 'cache_read_tokens')
op.drop_column('conversation_metadata', 'max_budget_per_task')
op.execute('DROP TYPE appconversationstarttaskstatus')
op.execute('DROP TYPE eventcallbackresultstatus')
# ### end Alembic commands ###
+231 -49
View File
@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiofiles"
@@ -148,6 +148,25 @@ files = [
frozenlist = ">=1.1.0"
typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""}
[[package]]
name = "aiosqlite"
version = "0.21.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "aiosqlite-0.21.0-py3-none-any.whl", hash = "sha256:2549cf4057f95f53dcba16f2b64e8e2791d7e1adedb13197dd8ed77bb226d7d0"},
{file = "aiosqlite-0.21.0.tar.gz", hash = "sha256:131bb8056daa3bc875608c631c678cda73922a2d4ba8aec373b19f18c17e7aa3"},
]
[package.dependencies]
typing_extensions = ">=4.0"
[package.extras]
dev = ["attribution (==1.7.1)", "black (==24.3.0)", "build (>=1.2)", "coverage[toml] (==7.6.10)", "flake8 (==7.0.0)", "flake8-bugbear (==24.12.12)", "flit (==3.10.1)", "mypy (==1.14.1)", "ufmt (==2.5.1)", "usort (==1.0.8.post1)"]
docs = ["sphinx (==8.1.3)", "sphinx-mdinclude (==0.6.1)"]
[[package]]
name = "alembic"
version = "1.16.5"
@@ -1061,7 +1080,7 @@ files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""}
[[package]]
name = "comm"
@@ -1797,6 +1816,25 @@ files = [
{file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"},
]
[[package]]
name = "ecdsa"
version = "0.19.1"
description = "ECDSA cryptographic signature library (pure python)"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.6"
groups = ["main"]
files = [
{file = "ecdsa-0.19.1-py2.py3-none-any.whl", hash = "sha256:30638e27cf77b7e15c4c4cc1973720149e1033827cfd00661ca5c8cc0cdb24c3"},
{file = "ecdsa-0.19.1.tar.gz", hash = "sha256:478cba7b62555866fcb3bb3fe985e06decbdb68ef55713c4e5ab98c57d508e61"},
]
[package.dependencies]
six = ">=1.9.0"
[package.extras]
gmpy = ["gmpy"]
gmpy2 = ["gmpy2"]
[[package]]
name = "email-validator"
version = "2.3.0"
@@ -1968,38 +2006,79 @@ websockets = ["websockets (>=15.0.1)"]
[[package]]
name = "fastuuid"
version = "0.12.0"
version = "0.13.5"
description = "Python bindings to Rust's UUID library."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "fastuuid-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:22a900ef0956aacf862b460e20541fdae2d7c340594fe1bd6fdcb10d5f0791a9"},
{file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0302f5acf54dc75de30103025c5a95db06d6c2be36829043a0aa16fc170076bc"},
{file = "fastuuid-0.12.0-cp310-cp310-manylinux_2_34_x86_64.whl", hash = "sha256:7946b4a310cfc2d597dcba658019d72a2851612a2cebb949d809c0e2474cf0a6"},
{file = "fastuuid-0.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:a1b6764dd42bf0c46c858fb5ade7b7a3d93b7a27485a7a5c184909026694cd88"},
{file = "fastuuid-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2bced35269315d16fe0c41003f8c9d63f2ee16a59295d90922cad5e6a67d0418"},
{file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82106e4b0a24f4f2f73c88f89dadbc1533bb808900740ca5db9bbb17d3b0c824"},
{file = "fastuuid-0.12.0-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:4db1bc7b8caa1d7412e1bea29b016d23a8d219131cff825b933eb3428f044dca"},
{file = "fastuuid-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:07afc8e674e67ac3d35a608c68f6809da5fab470fb4ef4469094fdb32ba36c51"},
{file = "fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1"},
{file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f"},
{file = "fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0"},
{file = "fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4"},
{file = "fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786"},
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c"},
{file = "fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37"},
{file = "fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9"},
{file = "fastuuid-0.12.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:2925f67b88d47cb16aa3eb1ab20fdcf21b94d74490e0818c91ea41434b987493"},
{file = "fastuuid-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7b15c54d300279ab20a9cc0579ada9c9f80d1bc92997fc61fb7bf3103d7cb26b"},
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:458f1bc3ebbd76fdb89ad83e6b81ccd3b2a99fa6707cd3650b27606745cfb170"},
{file = "fastuuid-0.12.0-cp38-cp38-manylinux_2_34_x86_64.whl", hash = "sha256:a8f0f83fbba6dc44271a11b22e15838641b8c45612cdf541b4822a5930f6893c"},
{file = "fastuuid-0.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:7cfd2092253d3441f6a8c66feff3c3c009da25a5b3da82bc73737558543632be"},
{file = "fastuuid-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9303617e887429c193d036d47d0b32b774ed3618431123e9106f610d601eb57e"},
{file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8790221325b376e1122e95f865753ebf456a9fb8faf0dca4f9bf7a3ff620e413"},
{file = "fastuuid-0.12.0-cp39-cp39-manylinux_2_34_x86_64.whl", hash = "sha256:e4b12d3e23515e29773fa61644daa660ceb7725e05397a986c2109f512579a48"},
{file = "fastuuid-0.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:e41656457c34b5dcb784729537ea64c7d9bbaf7047b480c6c6a64c53379f455a"},
{file = "fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e"},
{file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b9edf8ee30718aee787cdd2e9e1ff3d4a3ec6ddb32fba0a23fa04956df69ab07"},
{file = "fastuuid-0.13.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:f67ea1e25c5e782f7fb5aaa5208f157d950401dd9321ce56bcc6d4dc3d72ed60"},
{file = "fastuuid-0.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9ff3fc87e1f19603dd53c38f42c2ea8d5d5462554deab69e9cf1800574e4756c"},
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6e5337fa7698dc52bc724da7e9239e93c5b24a09f6904b8660dfb8c41ce3dee"},
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9db596023c10dabb12489a88c51b75297c3a2478cb2be645e06905934e7b9fc"},
{file = "fastuuid-0.13.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:191ff6192fe53c5fc9d4d241ee1156b30a7ed6f1677b1cc2423e7ecdbc26222b"},
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:348ce9f296dda701ba46d8dceeff309f90dbc75dd85080bbed2b299aa908890a"},
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:46954fb644995d7fc8bbd710fbd4c65cedaa48c921c86fdbafef0229168a8c96"},
{file = "fastuuid-0.13.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:22da0f66041e1c10c7d465b495cc6cd8e17e080dda34b4bd5ff5240b860fbb82"},
{file = "fastuuid-0.13.5-cp310-cp310-win32.whl", hash = "sha256:3e6b548f06c1ed7bad951a17a09eef69d6f24eb2b874cb4833e26b886d82990f"},
{file = "fastuuid-0.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:c82838e52189d16b1307631179cb2cd37778dd8f4ddc00e9ce3c26f920b3b2f7"},
{file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:c122558ca4b5487e2bd0863467e4ccfe636afd1274803741487d48f2e32ea0e1"},
{file = "fastuuid-0.13.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d7abd42a03a17a681abddd19aa4d44ca2747138cf8a48373b395cf1341a10de2"},
{file = "fastuuid-0.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2705cf7c2d6f7c03053404b75a4c44f872a73f6f9d5ea34f1dc6bba400c4a97c"},
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d220a056fcbad25932c1f25304261198612f271f4d150b2a84e81adb877daf7"},
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f29f93b5a0c5f5579f97f77d5319e9bfefd61d8678ec59d850201544faf33bf"},
{file = "fastuuid-0.13.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:399d86623fb806151b1feb9fdd818ebfc1d50387199a35f7264f98dfc1540af5"},
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:689e8795a1edd573b2c9a455024e4edf605a9690339bba29709857f7180894ea"},
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:25e82c4a1734da168b36f7308e397afbe9c9b353799a9c69563a605f11dd4641"},
{file = "fastuuid-0.13.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f62299e3cca69aad6a6fb37e26e45055587954d498ad98903fea24382377ea0e"},
{file = "fastuuid-0.13.5-cp311-cp311-win32.whl", hash = "sha256:68227f2230381b89fb1ad362ca6e433de85c6c11c36312b41757cad47b8a8e32"},
{file = "fastuuid-0.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:4a32306982bd031cb20d5d1a726b7b958a55babebd2300ce6c8e352d3496e931"},
{file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a"},
{file = "fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7"},
{file = "fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202"},
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e"},
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854"},
{file = "fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86"},
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952"},
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9"},
{file = "fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3"},
{file = "fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85"},
{file = "fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a"},
{file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa"},
{file = "fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6"},
{file = "fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a"},
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4"},
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817"},
{file = "fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e"},
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448"},
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471"},
{file = "fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83"},
{file = "fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021"},
{file = "fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f"},
{file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e353c0a0d978a5ecd97171ac4fb7f55a6bd6cbae90f1ec4e828e5317f11b995e"},
{file = "fastuuid-0.13.5-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:904ac3eb37f4742e23f6a51be0d0451d1d3aceb50df8dac7afc6bf5209793650"},
{file = "fastuuid-0.13.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c6b070e0dc1965d53b9e07c291537095ececf7d7e36e60aed9b22400fa6c5c7f"},
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0225f8bc78071a191cb458b3b0e23b04a7f03013575b8a3083da2a84c450e200"},
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3f3f8f10b962cf2e11d3affc0cf2697ac5c9accc0d282dce981ed555a44ce15"},
{file = "fastuuid-0.13.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1ae87968614fe6d3029a8198671b5893341aac9459289e93d201027be9ea7e8"},
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6ca01f5e614530a1a858bf185dd5556805a4c11b6eba0a2536890b68ed954922"},
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6d3b6b10d78b9f7056445ac377612443980349da7221a3dd3e3f382f7c437be3"},
{file = "fastuuid-0.13.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9ae95e4dcf94775d948ebb843f4443d33cd224bb31174030e106ee3cab66527c"},
{file = "fastuuid-0.13.5-cp38-cp38-win32.whl", hash = "sha256:5d753bc9ba8de6dd9caa8bbac045578c2fbe1c6ae40c2026b614676776fbe9dc"},
{file = "fastuuid-0.13.5-cp38-cp38-win_amd64.whl", hash = "sha256:f9530f1328b05b80c6fa111e7f2a5d55fa30fbbd72d708326d0c7b55b67ed772"},
{file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ed78153c589e5efb34faaa216836a5bf8a0b9d34e82183203166011238d9ed13"},
{file = "fastuuid-0.13.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a67e18c2d7fba8be6ea4aed8ca5a20fcf273f003efa01c1f33a096b72537e69e"},
{file = "fastuuid-0.13.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e362a3d66874d3d11a1ee9a8e717e32c2817cdb5d7a4e913290bf6e0f2a7fd8"},
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9baa9c33848ec0926231e7ecfef9e02faa0f6d24265b64108ea41f7a0bb3f48"},
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec6dab282162c19ec2172f33bafd467cffe26b92345789278adcbec19428d1"},
{file = "fastuuid-0.13.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2424a90688dbc44f119686fa452ff21aa106c9da258214f577816462ad606d5"},
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f8f2cc6972941ab030f3776961ed8454772c3acad88781fc262d71514df89973"},
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a41257ea172b5de199c3cfa71cc6c574dcf22367fe51e26cba0d359107f11f30"},
{file = "fastuuid-0.13.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:cade5f3b8023dbba5a006e5685a7baf0d7a30c43cea17113768aa9ef9582d799"},
{file = "fastuuid-0.13.5-cp39-cp39-win32.whl", hash = "sha256:880f0d03ad2518b96757ca422cba6ff76cea5464db2b3ad75c32acf1890e058f"},
{file = "fastuuid-0.13.5-cp39-cp39-win_amd64.whl", hash = "sha256:ebe95b730f81808eabc90247ac3d412b96d9fae1c406760b163bb9f134b7af69"},
{file = "fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1"},
]
[[package]]
@@ -4187,14 +4266,14 @@ dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[tom
[[package]]
name = "libtmux"
version = "0.39.0"
version = "0.46.2"
description = "Typed library that provides an ORM wrapper for tmux, a terminal multiplexer."
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "libtmux-0.39.0-py3-none-any.whl", hash = "sha256:6b6e338be2727f67aa6b7eb67fa134368fa3c3eac5df27565396467692891c1e"},
{file = "libtmux-0.39.0.tar.gz", hash = "sha256:59346aeef3c0d6017f3bc5e23248d43cdf50f32b775b9cb5d9ff5e2e5f3059f4"},
{file = "libtmux-0.46.2-py3-none-any.whl", hash = "sha256:6c32dbf22bde8e5e33b2714a4295f6e838dc640f337cd4c085a044f6828c7793"},
{file = "libtmux-0.46.2.tar.gz", hash = "sha256:9a398fec5d714129c8344555d466e1a903dfc0f741ba07aabe75a8ceb25c5dda"},
]
[[package]]
@@ -4228,26 +4307,24 @@ valkey = ["valkey (>=6)"]
[[package]]
name = "litellm"
version = "1.76.1"
version = "1.77.7"
description = "Library to easily interface with LLM API providers"
optional = false
python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8"
python-versions = ">=3.8.1,<4.0, !=3.9.7"
groups = ["main"]
files = [
{file = "litellm-1.76.1-py3-none-any.whl", hash = "sha256:938f05075372f26098211ea9b3cb0a6bb7b46111330226b70d42d40bd307812f"},
{file = "litellm-1.76.1.tar.gz", hash = "sha256:d5a3a3efda04999b60ec0d1c29c1eaaa12f89a7b29db4bda691c7fb55b4fa6ad"},
]
files = []
develop = false
[package.dependencies]
aiohttp = ">=3.10"
click = "*"
fastuuid = ">=0.12.0"
fastuuid = ">=0.13.0"
httpx = ">=0.23.0"
importlib-metadata = ">=6.8.0"
jinja2 = ">=3.1.2,<4.0.0"
jsonschema = ">=4.22.0,<5.0.0"
jinja2 = "^3.1.2"
jsonschema = "^4.22.0"
openai = ">=1.99.5"
pydantic = ">=2.5.0,<3.0.0"
pydantic = "^2.5.0"
python-dotenv = ">=0.2.0"
tiktoken = ">=0.7.0"
tokenizers = "*"
@@ -4256,10 +4333,16 @@ tokenizers = "*"
caching = ["diskcache (>=5.6.1,<6.0.0)"]
extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-iam (>=2.19.1,<3.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0) ; python_version >= \"3.9\" and python_version < \"3.14\"", "resend (>=0.8.0,<0.9.0)"]
mlflow = ["mlflow (>3.1.4) ; python_version >= \"3.10\""]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.19)", "litellm-proxy-extras (==0.2.18)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.36.0)", "cryptography", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.20)", "litellm-proxy-extras (==0.2.25)", "mcp (>=1.10.0,<2.0.0) ; python_version >= \"3.10\"", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0) ; python_version >= \"3.10\"", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0) ; sys_platform != \"win32\"", "websockets (>=13.1.0,<14.0.0)"]
semantic-router = ["semantic-router ; python_version >= \"3.9\""]
utils = ["numpydoc"]
[package.source]
type = "git"
url = "https://github.com/BerriAI/litellm.git"
reference = "v1.77.7.dev9"
resolved_reference = "763d2f8ccdd8412dbe6d4ac0e136d9ac34dcd4c0"
[[package]]
name = "llvmlite"
version = "0.44.0"
@@ -5430,9 +5513,36 @@ youtube-transcript-api = ">=0.6.2"
[package.extras]
llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0)", "llama-index-retrievers-bm25 (>=0.5.2,<0.6.0)"]
[[package]]
name = "openhands-agent-server"
version = "1.0.0"
description = "OpenHands Agent Server - REST/WebSocket interface for OpenHands AI Agent"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = []
develop = false
[package.dependencies]
aiosqlite = ">=0.19"
alembic = ">=1.13"
docker = ">=7.1,<8"
fastapi = ">=0.104"
pydantic = ">=2"
sqlalchemy = ">=2"
uvicorn = ">=0.31.1"
websockets = ">=12"
[package.source]
type = "git"
url = "https://github.com/All-Hands-AI/agent-sdk.git"
reference = "08cf609a996523c0199c61c768d74417b7e96109"
resolved_reference = "08cf609a996523c0199c61c768d74417b7e96109"
subdirectory = "openhands/agent_server"
[[package]]
name = "openhands-ai"
version = "0.57.0"
version = "0.59.0"
description = "OpenHands: Code Less, Make More"
optional = false
python-versions = "^3.12,<3.14"
@@ -5444,6 +5554,7 @@ develop = true
aiohttp = ">=3.9.0,!=3.11.13"
anthropic = {version = "*", extras = ["vertex"]}
anyio = "4.9.0"
asyncpg = "^0.30.0"
bashlex = "^0.18"
boto3 = "*"
browsergym-core = "0.13.3"
@@ -5465,21 +5576,26 @@ joblib = "*"
json-repair = "*"
jupyter_kernel_gateway = "*"
kubernetes = "^33.1.0"
libtmux = ">=0.37,<0.40"
litellm = ">=1.74.3, <1.77.2, !=1.64.4, !=1.67.*"
libtmux = ">=0.46.2"
litellm = ">=1.74.3, <1.78.0, !=1.64.4, !=1.67.*"
memory-profiler = "^0.61.0"
numpy = "*"
openai = "1.99.9"
openhands-aci = "0.3.2"
openhands-agent-server = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "08cf609a996523c0199c61c768d74417b7e96109", subdirectory = "openhands/agent_server"}
openhands-sdk = {git = "https://github.com/All-Hands-AI/agent-sdk.git", rev = "08cf609a996523c0199c61c768d74417b7e96109", subdirectory = "openhands/sdk"}
opentelemetry-api = "^1.33.1"
opentelemetry-exporter-otlp-proto-grpc = "^1.33.1"
pathspec = "^0.12.1"
pexpect = "*"
pg8000 = "^1.31.5"
pillow = "^11.3.0"
playwright = "^1.55.0"
poetry = "^2.1.2"
prompt-toolkit = "^3.0.50"
protobuf = "^5.0.0,<6.0.0"
psutil = "*"
pybase62 = "^1.0.0"
pygithub = "^2.5.0"
pyjwt = "^2.9.0"
pylatexenc = "*"
@@ -5488,6 +5604,7 @@ PyPDF2 = "*"
python-docx = "*"
python-dotenv = "*"
python-frontmatter = "^1.1.0"
python-jose = {version = ">=3.3", extras = ["cryptography"]}
python-json-logger = "^3.2.1"
python-multipart = "*"
python-pptx = "*"
@@ -5500,6 +5617,7 @@ redis = ">=5.2,<7.0"
requests = "^2.32.5"
setuptools = ">=78.1.1"
shellingham = "^1.5.4"
sqlalchemy = {version = "^2.0.40", extras = ["asyncio"]}
sse-starlette = "^3.0.2"
starlette = "^0.48.0"
tenacity = ">=8.5,<10.0"
@@ -5519,6 +5637,35 @@ third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2.0.0,<3.
type = "directory"
url = ".."
[[package]]
name = "openhands-sdk"
version = "1.0.0"
description = "OpenHands SDK - Core functionality for building AI agents"
optional = false
python-versions = ">=3.12"
groups = ["main"]
files = []
develop = false
[package.dependencies]
fastmcp = ">=2.11.3"
litellm = {git = "https://github.com/BerriAI/litellm.git", rev = "v1.77.7.dev9"}
pydantic = ">=2.11.7"
python-frontmatter = ">=1.1.0"
python-json-logger = ">=3.3.0"
tenacity = ">=9.1.2"
websockets = ">=12"
[package.extras]
boto3 = ["boto3 (>=1.35.0)"]
[package.source]
type = "git"
url = "https://github.com/All-Hands-AI/agent-sdk.git"
reference = "08cf609a996523c0199c61c768d74417b7e96109"
resolved_reference = "08cf609a996523c0199c61c768d74417b7e96109"
subdirectory = "openhands/sdk"
[[package]]
name = "openpyxl"
version = "3.1.5"
@@ -5855,14 +6002,14 @@ ptyprocess = ">=0.5"
[[package]]
name = "pg8000"
version = "1.31.4"
version = "1.31.5"
description = "PostgreSQL interface library"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pg8000-1.31.4-py3-none-any.whl", hash = "sha256:d14fb2054642ee80f9a216721892e99e19db60a005358460ffa48872351423d4"},
{file = "pg8000-1.31.4.tar.gz", hash = "sha256:e7ecce4339891f27b0b22e2f79eb9efe44118bd384207359fc18350f788ace00"},
{file = "pg8000-1.31.5-py3-none-any.whl", hash = "sha256:0af2c1926b153307639868d2ee5cef6cd3a7d07448e12736989b10e1d491e201"},
{file = "pg8000-1.31.5.tar.gz", hash = "sha256:46ebb03be52b7a77c03c725c79da2ca281d6e8f59577ca66b17c9009618cae78"},
]
[package.dependencies]
@@ -6528,6 +6675,17 @@ files = [
[package.dependencies]
pyasn1 = ">=0.6.1,<0.7.0"
[[package]]
name = "pybase62"
version = "1.0.0"
description = "Python module for base62 encoding"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "pybase62-1.0.0-py3-none-any.whl", hash = "sha256:60539ad956ec9e9de091bc7ae88c9550bc2fa17f503050cf34d021b75e73cb27"},
]
[[package]]
name = "pycodestyle"
version = "2.14.0"
@@ -7122,6 +7280,30 @@ PyYAML = "*"
docs = ["sphinx"]
test = ["mypy", "pyaml", "pytest", "toml", "types-PyYAML", "types-toml"]
[[package]]
name = "python-jose"
version = "3.5.0"
description = "JOSE implementation in Python"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "python_jose-3.5.0-py2.py3-none-any.whl", hash = "sha256:abd1202f23d34dfad2c3d28cb8617b90acf34132c7afd60abd0b0b7d3cb55771"},
{file = "python_jose-3.5.0.tar.gz", hash = "sha256:fb4eaa44dbeb1c26dcc69e4bd7ec54a1cb8dd64d3b4d81ef08d90ff453f2b01b"},
]
[package.dependencies]
cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"cryptography\""}
ecdsa = "!=0.15"
pyasn1 = ">=0.5.0"
rsa = ">=4.0,<4.1.1 || >4.1.1,<4.4 || >4.4,<5.0"
[package.extras]
cryptography = ["cryptography (>=3.4.0)"]
pycrypto = ["pycrypto (>=2.6.0,<2.7.0)"]
pycryptodome = ["pycryptodome (>=3.3.1,<4.0.0)"]
test = ["pytest", "pytest-cov"]
[[package]]
name = "python-json-logger"
version = "3.3.0"
+12 -1
View File
@@ -13,7 +13,8 @@ from server.auth.auth_error import (
ExpiredError,
NoCredentialsError,
)
from server.auth.token_manager import TokenManager, get_config
from server.auth.token_manager import TokenManager
from server.config import get_config
from server.logger import logger
from server.rate_limit import RateLimiter, create_redis_rate_limiter
from storage.api_key_store import ApiKeyStore
@@ -223,6 +224,16 @@ class SaasUserAuth(UserAuth):
await rate_limiter.hit('auth_uid', user_id)
return instance
@classmethod
async def get_for_user(cls, user_id: str) -> UserAuth:
offline_token = await token_manager.load_offline_token(user_id)
assert offline_token is not None
return SaasUserAuth(
user_id=user_id,
refresh_token=SecretStr(offline_token),
auth_type=AuthType.BEARER,
)
def get_api_key_from_header(request: Request):
auth_header = request.headers.get('Authorization')
+1 -11
View File
@@ -26,6 +26,7 @@ from server.auth.constants import (
KEYCLOAK_SERVER_URL_EXT,
)
from server.auth.keycloak_manager import get_keycloak_admin, get_keycloak_openid
from server.config import get_config
from server.logger import logger
from sqlalchemy import String as SQLString
from sqlalchemy import type_coerce
@@ -35,19 +36,8 @@ from storage.github_app_installation import GithubAppInstallation
from storage.offline_token_store import OfflineTokenStore
from tenacity import RetryCallState, retry, retry_if_exception_type, stop_after_attempt
from openhands.core.config import load_openhands_config
from openhands.integrations.service_types import ProviderType
# Create a function to get config to avoid circular imports
_config = None
def get_config():
global _config
if _config is None:
_config = load_openhands_config()
return _config
def _before_sleep_callback(retry_state: RetryCallState) -> None:
logger.info(f'Retry attempt {retry_state.attempt_number} for Keycloak operation')
+11
View File
@@ -19,10 +19,21 @@ from server.auth.constants import (
GITLAB_APP_CLIENT_ID,
)
from openhands.core.config.utils import load_openhands_config
from openhands.integrations.service_types import ProviderType
from openhands.server.config.server_config import ServerConfig
from openhands.server.types import AppMode
# Create a function to get config to avoid circular imports
_config = None
def get_config():
global _config
if _config is None:
_config = load_openhands_config()
return _config
def sign_token(payload: dict[str, object], jwt_secret: str, algorithm='HS256') -> str:
"""Signs a JWT token."""
+1 -1
View File
@@ -424,7 +424,7 @@ async def refresh_tokens(
provider_handler = ProviderHandler(
create_provider_tokens_object([provider]), external_auth_id=user_id
)
service = provider_handler._get_service(provider)
service = provider_handler.get_service(provider)
token = await service.get_latest_token()
if not token:
raise HTTPException(
@@ -784,6 +784,7 @@ class SaasNestedConversationManager(ConversationManager):
env_vars['SKIP_DEPENDENCY_CHECK'] = '1'
env_vars['INITIAL_NUM_WARM_SERVERS'] = '1'
env_vars['INIT_GIT_IN_EMPTY_WORKSPACE'] = '1'
env_vars['ENABLE_V1'] = '0'
# We need this for LLM traces tracking to identify the source of the LLM calls
env_vars['WEB_HOST'] = WEB_HOST
@@ -195,14 +195,11 @@ def update_active_working_seconds(
file_store: The FileStore instance for accessing conversation data
"""
try:
# Get all events for the conversation
events = list(event_store.get_events())
# Track agent state changes and calculate running time
running_start_time = None
total_running_seconds = 0.0
for event in events:
for event in event_store.search_events():
if isinstance(event, AgentStateChangedObservation) and event.timestamp:
event_timestamp = datetime.fromisoformat(event.timestamp).timestamp()
+2 -2
View File
@@ -2,6 +2,6 @@
Unified SQLAlchemy declarative base for all models.
"""
from sqlalchemy.orm import declarative_base
from openhands.app_server.utils.sql_utils import Base
Base = declarative_base()
__all__ = ['Base']
+4 -1
View File
@@ -1,7 +1,6 @@
import asyncio
import os
from google.cloud.sql.connector import Connector
from sqlalchemy import create_engine
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.orm import sessionmaker
@@ -26,6 +25,8 @@ def _get_db_engine():
if GCP_DB_INSTANCE: # GCP environments
def get_db_connection():
from google.cloud.sql.connector import Connector
connector = Connector()
instance_string = f'{GCP_PROJECT}:{GCP_REGION}:{GCP_DB_INSTANCE}'
return connector.connect(
@@ -52,6 +53,8 @@ def _get_db_engine():
async def async_creator():
from google.cloud.sql.connector import Connector
loop = asyncio.get_running_loop()
async with Connector(loop=loop) as connector:
conn = await connector.connect_async(
@@ -52,6 +52,14 @@ class SaasConversationStore(ConversationStore):
# Convert string to ProviderType enum
kwargs['git_provider'] = ProviderType(kwargs['git_provider'])
# Remove V1 attributes
kwargs.pop('max_budget_per_task', None)
kwargs.pop('cache_read_tokens', None)
kwargs.pop('cache_write_tokens', None)
kwargs.pop('reasoning_tokens', None)
kwargs.pop('context_window', None)
kwargs.pop('per_turn_token', None)
return ConversationMetadata(**kwargs)
async def save_metadata(self, metadata: ConversationMetadata):
@@ -1,41 +1,8 @@
import uuid
from datetime import UTC, datetime
from openhands.app_server.app_conversation.sql_app_conversation_info_service import (
StoredConversationMetadata as _StoredConversationMetadata,
)
from sqlalchemy import JSON, Column, DateTime, Float, Integer, String
from storage.base import Base
StoredConversationMetadata = _StoredConversationMetadata
class StoredConversationMetadata(Base): # type: ignore
__tablename__ = 'conversation_metadata'
conversation_id = Column(
String, primary_key=True, default=lambda: str(uuid.uuid4())
)
github_user_id = Column(String, nullable=True) # The GitHub user ID
user_id = Column(String, nullable=False) # The Keycloak User ID
selected_repository = Column(String, nullable=True)
selected_branch = Column(String, nullable=True)
git_provider = Column(
String, nullable=True
) # The git provider (GitHub, GitLab, etc.)
title = Column(String, nullable=True)
last_updated_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC), # type: ignore[attr-defined]
)
created_at = Column(
DateTime(timezone=True),
default=lambda: datetime.now(UTC), # type: ignore[attr-defined]
)
trigger = Column(String, nullable=True)
pr_number = Column(
JSON, nullable=True
) # List of PR numbers associated with the conversation
# Cost and token metrics
accumulated_cost = Column(Float, default=0.0)
prompt_tokens = Column(Integer, default=0)
completion_tokens = Column(Integer, default=0)
total_tokens = Column(Integer, default=0)
# LLM model used for the conversation
llm_model = Column(String, nullable=True)
__all__ = ['StoredConversationMetadata']
@@ -80,7 +80,7 @@ class TestUpdateActiveWorkingSeconds:
events.append(event6)
# Configure the mock event store to return our test events
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -133,7 +133,7 @@ class TestUpdateActiveWorkingSeconds:
events = [event1, event2]
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -178,7 +178,7 @@ class TestUpdateActiveWorkingSeconds:
events = [event1, event2, event3]
# No final state change - agent still running
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -221,7 +221,7 @@ class TestUpdateActiveWorkingSeconds:
events = [event1, event2, event3]
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -267,7 +267,7 @@ class TestUpdateActiveWorkingSeconds:
events = [event1, event2, event3, event4]
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -297,7 +297,7 @@ class TestUpdateActiveWorkingSeconds:
user_id = 'test_user_error'
# Configure the mock to raise an exception
mock_event_store.get_events.side_effect = Exception('Test error')
mock_event_store.search_events.side_effect = Exception('Test error')
# Call the function under test
update_active_working_seconds(
@@ -376,7 +376,7 @@ class TestUpdateActiveWorkingSeconds:
event10.timestamp = '1970-01-01T00:00:37.000000'
events.append(event10)
mock_event_store.get_events.return_value = events
mock_event_store.search_events.return_value = events
# Call the function under test with mocked session_maker
with patch(
@@ -20,7 +20,7 @@ def token_store(session_maker, mock_config):
@pytest.fixture
def token_manager():
with patch('server.auth.token_manager.get_config') as mock_get_config:
with patch('server.config.get_config') as mock_get_config:
mock_config = mock_get_config.return_value
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
return TokenManager(external=False)
@@ -8,7 +8,7 @@ from openhands.integrations.service_types import ProviderType
@pytest.fixture
def token_manager():
with patch('server.auth.token_manager.get_config') as mock_get_config:
with patch('server.config.get_config') as mock_get_config:
mock_config = mock_get_config.return_value
mock_config.jwt_secret.get_secret_value.return_value = 'test_secret'
return TokenManager(external=False)
@@ -307,7 +307,7 @@ class TheoremqaTask(Task):
# Converting the string answer to a number/list/bool/option
try:
prediction = eval(prediction)
prediction = ast.literal_eval(prediction)
except Exception:
LOGGER.warning(
f'[TASK] Failed to convert the answer: {prediction}\n{traceback.format_exc()}'
@@ -111,15 +111,10 @@ for run_idx in $(seq 1 $N_RUNS); do
echo "### Evaluating on $OUTPUT_FILE ... ###"
OUTPUT_CONFIG_FILE="${OUTPUT_FILE%.jsonl}_config.json"
export EVAL_SKIP_BUILD_ERRORS=true
pip install multi-swe-bench --quiet --disable-pip-version-check > /dev/null 2>&1
COMMAND="poetry run python ./evaluation/benchmarks/multi_swe_bench/scripts/eval/update_multi_swe_bench_config.py --input $OUTPUT_FILE --output $OUTPUT_CONFIG_FILE --dataset $EVAL_DATASET;
python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
poetry run python -m multi_swe_bench.harness.run_evaluation --config $OUTPUT_CONFIG_FILE
"
if [ -n "$EVAL_LIMIT" ]; then
echo "EVAL_LIMIT: $EVAL_LIMIT"
COMMAND="$COMMAND --eval-n-limit $EVAL_LIMIT"
fi
echo "Running command: $COMMAND"
# Run the command
eval $COMMAND
@@ -16,6 +16,10 @@ At the end, you must test your code rigorously using the tools provided, and do
You MUST plan extensively before each function call, and reflect extensively on the outcomes of the previous function calls. DO NOT do this entire process by making function calls only, as this can impair your ability to solve the problem and think insightfully.
## Issue Description
{{ instance.problem_statement }}
# Workflow
## High-Level Problem Solving Strategy
@@ -73,6 +77,7 @@ Carefully read the issue and think hard about a plan to solve it before coding.
## 8. Final Reflection and Additional Testing
- Reflect carefully on the original intent of the user and the problem statement.
- Compare your changes with the base commit {{ instance.base_commit }} to ensure minimal and focused modifications.
- Think about potential edge cases or scenarios that may not be covered by existing tests.
- Write additional tests that would need to pass to fully validate the correctness of your solution.
- Run these new tests and ensure they all pass.
+3 -2
View File
@@ -24,8 +24,8 @@ from openhands.controller.state.state import State
from openhands.core.config import (
AgentConfig,
OpenHandsConfig,
get_evaluation_parser,
get_llm_config_arg,
parse_arguments,
)
from openhands.core.logger import openhands_logger as logger
from openhands.core.main import create_runtime, run_controller
@@ -166,7 +166,8 @@ def load_integration_tests() -> pd.DataFrame:
if __name__ == '__main__':
args = parse_arguments()
parser = get_evaluation_parser()
args, _ = parser.parse_known_args()
integration_tests = load_integration_tests()
llm_config = None
+60
View File
@@ -0,0 +1,60 @@
import { act } from "@testing-library/react";
import { vi, afterEach } from "vitest";
import type * as ZustandExportedTypes from "zustand";
export * from "zustand";
const { create: actualCreate, createStore: actualCreateStore } =
await vi.importActual<typeof ZustandExportedTypes>("zustand");
// a variable to hold reset functions for all stores declared in the app
export const storeResetFns = new Set<() => void>();
const createUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreate(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};
// when creating a store, we get its initial state, create a reset function and add it in the set
export const create = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) =>
// to support curried version of create
typeof stateCreator === "function"
? createUncurried(stateCreator)
: createUncurried) as typeof ZustandExportedTypes.create;
const createStoreUncurried = <T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) => {
const store = actualCreateStore(stateCreator);
const initialState = store.getInitialState();
storeResetFns.add(() => {
store.setState(initialState, true);
});
return store;
};
// when creating a store, we get its initial state, create a reset function and add it in the set
export const createStore = (<T>(
stateCreator: ZustandExportedTypes.StateCreator<T>,
) =>
// to support curried version of createStore
typeof stateCreator === "function"
? createStoreUncurried(stateCreator)
: createStoreUncurried) as typeof ZustandExportedTypes.createStore;
// reset all stores after each test run
afterEach(() => {
act(() => {
storeResetFns.forEach((resetFn) => {
resetFn();
});
});
});
@@ -23,6 +23,7 @@ import { useConfig } from "#/hooks/query/use-config";
import { useGetTrajectory } from "#/hooks/mutation/use-get-trajectory";
import { useUploadFiles } from "#/hooks/mutation/use-upload-files";
import { OpenHandsAction } from "#/types/core/actions";
import { useEventStore } from "#/stores/use-event-store";
// Mock the hooks
vi.mock("#/context/ws-client-provider");
@@ -176,7 +177,7 @@ describe("ChatInterface - Chat Suggestions", () => {
});
test("should hide chat suggestions when there is a user message", () => {
const userEvent: OpenHandsAction = {
const mockUserEvent: OpenHandsAction = {
id: 1,
source: "user",
action: "message",
@@ -189,10 +190,11 @@ describe("ChatInterface - Chat Suggestions", () => {
timestamp: "2025-07-01T00:00:00Z",
};
(useWsClient as unknown as ReturnType<typeof vi.fn>).mockReturnValue({
send: vi.fn(),
isLoadingMessages: false,
parsedEvents: [userEvent],
useEventStore.setState({
events: [mockUserEvent],
uiEvents: [],
addEvent: vi.fn(),
clearEvents: vi.fn(),
});
renderWithQueryClient(<ChatInterface />, queryClient);
@@ -0,0 +1,397 @@
import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest";
import { screen, waitFor, render } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import {
createMockMessageEvent,
createMockUserMessageEvent,
createMockAgentErrorEvent,
} from "#/mocks/mock-ws-helpers";
import {
ConnectionStatusComponent,
EventStoreComponent,
OptimisticUserMessageStoreComponent,
ErrorMessageStoreComponent,
} from "./helpers/websocket-test-components";
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
import { conversationWebSocketTestSetup } from "./helpers/msw-websocket-setup";
// MSW WebSocket mock setup
const { wsLink, server: mswServer } = conversationWebSocketTestSetup();
beforeAll(() => mswServer.listen());
afterEach(() => {
mswServer.resetHandlers();
});
afterAll(() => mswServer.close());
// Helper function to render components with ConversationWebSocketProvider
function renderWithWebSocketContext(
children: React.ReactNode,
conversationId = "test-conversation-default",
) {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return render(
<QueryClientProvider client={queryClient}>
<ConversationWebSocketProvider conversationId={conversationId}>
{children}
</ConversationWebSocketProvider>
</QueryClientProvider>,
);
}
describe("Conversation WebSocket Handler", () => {
// 1. Connection Lifecycle Tests
describe("Connection Management", () => {
it("should establish WebSocket connection to /events/socket URL", async () => {
// This will fail because we haven't created the context yet
renderWithWebSocketContext(<ConnectionStatusComponent />);
// Initially should be CONNECTING
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"CONNECTING",
);
// Wait for connection to be established
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
});
it.todo("should provide manual disconnect functionality");
});
// 2. Event Processing Tests
describe("Event Stream Processing", () => {
it("should update event store with received WebSocket events", async () => {
// Create a mock MessageEvent to send through WebSocket
const mockMessageEvent = createMockMessageEvent();
// Set up MSW to send the event when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the mock event after connection
client.send(JSON.stringify(mockMessageEvent));
}),
);
// Render components that use both WebSocket and event store
renderWithWebSocketContext(<EventStoreComponent />);
// Wait for connection and event processing
await waitFor(() => {
expect(screen.getByTestId("events-count")).toHaveTextContent("1");
});
// Verify the event was added to the store
expect(screen.getByTestId("latest-event-id")).toHaveTextContent(
"test-event-123",
);
expect(screen.getByTestId("ui-events-count")).toHaveTextContent("1");
});
it("should handle malformed/invalid event data gracefully", async () => {
// Set up MSW to send various invalid events when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send invalid JSON
client.send("invalid json string");
// Send valid JSON but missing required fields
client.send(JSON.stringify({ message: "missing required fields" }));
// Send valid JSON with wrong data types
client.send(
JSON.stringify({
id: 123, // should be string
timestamp: "2023-01-01T00:00:00Z",
source: "agent",
}),
);
// Send null values for required fields
client.send(
JSON.stringify({
id: null,
timestamp: "2023-01-01T00:00:00Z",
source: "agent",
}),
);
// Send a valid event after invalid ones to ensure processing continues
client.send(
JSON.stringify({
id: "valid-event-123",
timestamp: new Date().toISOString(),
source: "agent",
llm_message: {
role: "assistant",
content: [
{ type: "text", text: "Valid message after invalid ones" },
],
},
activated_microagents: [],
extended_content: [],
}),
);
}),
);
// Render components that use both WebSocket and event store
renderWithWebSocketContext(<EventStoreComponent />);
// Wait for connection and event processing
// Only the valid event should be added to the store
await waitFor(() => {
expect(screen.getByTestId("events-count")).toHaveTextContent("1");
});
// Verify only the valid event was added
expect(screen.getByTestId("latest-event-id")).toHaveTextContent(
"valid-event-123",
);
expect(screen.getByTestId("ui-events-count")).toHaveTextContent("1");
});
});
// 3. State Management Tests
describe("State Management Integration", () => {
it("should clear optimistic user messages when confirmed", async () => {
// First, set an optimistic user message
const { setOptimisticUserMessage } =
useOptimisticUserMessageStore.getState();
setOptimisticUserMessage("This is an optimistic message");
// Create a mock user MessageEvent to send through WebSocket
const mockUserMessageEvent = createMockUserMessageEvent();
// Set up MSW to send the user message event when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the mock user message event after connection
client.send(JSON.stringify(mockUserMessageEvent));
}),
);
// Render components that use both WebSocket and optimistic user message store
renderWithWebSocketContext(<OptimisticUserMessageStoreComponent />);
// Initially should show the optimistic message
expect(screen.getByTestId("optimistic-user-message")).toHaveTextContent(
"This is an optimistic message",
);
// Wait for connection and user message event processing
// The optimistic message should be cleared when user message is confirmed
await waitFor(() => {
expect(screen.getByTestId("optimistic-user-message")).toHaveTextContent(
"none",
);
});
});
});
// 4. Cache Management Tests
describe("Cache Management", () => {
it.todo(
"should invalidate file changes cache on file edit/write/command events",
);
it.todo("should invalidate specific file diff cache on file modifications");
it.todo("should prevent cache refetch during high message rates");
it.todo("should not invalidate cache for non-file-related events");
it.todo("should invalidate cache with correct conversation ID context");
});
// 5. Error Handling Tests
describe("Error Handling & Recovery", () => {
it("should update error message store on AgentErrorEvent", async () => {
// Create a mock AgentErrorEvent to send through WebSocket
const mockAgentErrorEvent = createMockAgentErrorEvent();
// Set up MSW to send the error event when connection is established
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Send the mock error event after connection
client.send(JSON.stringify(mockAgentErrorEvent));
}),
);
// Render components that use both WebSocket and error message store
renderWithWebSocketContext(<ErrorMessageStoreComponent />);
// Initially should show "none"
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
// Wait for connection and error event processing
await waitFor(() => {
expect(screen.getByTestId("error-message")).toHaveTextContent(
"Failed to execute command: Permission denied",
);
});
});
it("should set error message store on WebSocket connection errors", async () => {
// Set up MSW to simulate connection error
mswServer.use(
wsLink.addEventListener("connection", ({ client }) => {
// Simulate connection error by closing immediately
client.close(1006, "Connection failed");
}),
);
// Render components that use both WebSocket and error message store
renderWithWebSocketContext(
<>
<ErrorMessageStoreComponent />
<ConnectionStatusComponent />
</>,
);
// Initially should show "none"
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
// Wait for connection error and error message to be set
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"CLOSED",
);
});
// Should set error message on connection failure
await waitFor(() => {
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
"none",
);
});
});
it("should set error message store on WebSocket disconnect with error", async () => {
// Set up MSW to connect first, then disconnect with error
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
server.connect();
// Simulate disconnect with error after a short delay
setTimeout(() => {
client.close(1006, "Unexpected disconnect");
}, 100);
}),
);
// Render components that use both WebSocket and error message store
renderWithWebSocketContext(
<>
<ErrorMessageStoreComponent />
<ConnectionStatusComponent />
</>,
);
// Initially should show "none"
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
// Wait for connection to be established first
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"OPEN",
);
});
// Wait for disconnect and error message to be set
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"CLOSED",
);
});
// Should set error message on unexpected disconnect
await waitFor(() => {
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
"none",
);
});
});
it("should clear error message store when connection is restored", async () => {
let connectionAttempt = 0;
// Set up MSW to fail first connection, then succeed on retry
mswServer.use(
wsLink.addEventListener("connection", ({ client, server }) => {
connectionAttempt += 1;
if (connectionAttempt === 1) {
// First attempt fails
client.close(1006, "Initial connection failed");
} else {
// Second attempt succeeds
server.connect();
}
}),
);
// Render components that use both WebSocket and error message store
renderWithWebSocketContext(
<>
<ErrorMessageStoreComponent />
<ConnectionStatusComponent />
</>,
);
// Initially should show "none"
expect(screen.getByTestId("error-message")).toHaveTextContent("none");
// Wait for first connection failure and error message
await waitFor(() => {
expect(screen.getByTestId("connection-state")).toHaveTextContent(
"CLOSED",
);
});
await waitFor(() => {
expect(screen.getByTestId("error-message")).not.toHaveTextContent(
"none",
);
});
// Simulate reconnection attempt (this would normally be triggered by the WebSocket context)
// For now, we'll just verify the pattern - when connection is restored, error should clear
// This test will fail until the WebSocket handler implements the clear logic
// Note: This test demonstrates the expected behavior but may need adjustment
// based on how the actual reconnection logic is implemented
});
it.todo("should track and display errors with proper metadata");
it.todo("should set appropriate error states on connection failures");
it.todo(
"should handle WebSocket close codes appropriately (1000, 1006, etc.)",
);
});
// 6. Connection State Validation Tests
describe("Connection State Management", () => {
it.todo("should only connect when conversation is in RUNNING status");
it.todo("should handle STARTING conversation state appropriately");
it.todo("should disconnect when conversation is STOPPED");
it.todo("should validate runtime status before connecting");
});
// 7. Message Sending Tests
describe("Message Sending", () => {
it.todo("should send user actions through WebSocket when connected");
it.todo("should handle send attempts when disconnected");
});
});
+46
View File
@@ -0,0 +1,46 @@
# Test Helpers
This directory contains reusable test utilities and components for the OpenHands frontend test suite.
## Files
### `websocket-test-components.tsx`
Contains React test components for accessing and displaying WebSocket-related store values:
- `ConnectionStatusComponent` - Displays WebSocket connection state
- `EventStoreComponent` - Displays event store values (events count, UI events count, latest event ID)
- `OptimisticUserMessageStoreComponent` - Displays optimistic user message store values
- `ErrorMessageStoreComponent` - Displays error message store values
These components are designed to be used in tests to verify that WebSocket events are properly processed and stored.
### `msw-websocket-setup.ts`
Contains MSW (Mock Service Worker) setup utilities for WebSocket testing:
- `createWebSocketLink()` - Creates a WebSocket link for MSW testing
- `createWebSocketMockServer()` - Creates and configures an MSW server for WebSocket testing
- `createWebSocketTestSetup()` - Creates a complete WebSocket testing setup
- `conversationWebSocketTestSetup()` - Standard setup for conversation WebSocket handler tests
## Usage
```typescript
import {
ConnectionStatusComponent,
EventStoreComponent,
} from "./__tests__/helpers/websocket-test-components";
import { conversationWebSocketTestSetup } from "./__tests__/helpers/msw-websocket-setup";
// Set up MSW server
const { wsLink, server } = conversationWebSocketTestSetup();
// Render components with WebSocket context (helper function defined in test file)
renderWithWebSocketContext(<ConnectionStatusComponent />);
```
## Benefits
- **Reusability**: Test components and utilities can be shared across multiple test files
- **Maintainability**: Changes to test setup only need to be made in one place
- **Consistency**: Ensures consistent test setup across different WebSocket-related tests
- **Readability**: Test files are cleaner and focus on test logic rather than setup boilerplate
@@ -0,0 +1,42 @@
import { ws } from "msw";
import { setupServer } from "msw/node";
/**
* Creates a WebSocket link for MSW testing
* @param url - WebSocket URL to mock (default: "ws://localhost/events/socket")
* @returns MSW WebSocket link
*/
export const createWebSocketLink = (url = "ws://localhost/events/socket") =>
ws.link(url);
/**
* Creates and configures an MSW server for WebSocket testing
* @param wsLink - WebSocket link to use for the server
* @returns Configured MSW server
*/
export const createWebSocketMockServer = (wsLink: ReturnType<typeof ws.link>) =>
setupServer(
wsLink.addEventListener("connection", ({ server }) => {
server.connect();
}),
);
/**
* Creates a complete WebSocket testing setup with server and link
* @param url - WebSocket URL to mock (default: "ws://localhost/events/socket")
* @returns Object containing the WebSocket link and configured server
*/
export const createWebSocketTestSetup = (
url = "ws://localhost/events/socket",
) => {
const wsLink = createWebSocketLink(url);
const server = createWebSocketMockServer(wsLink);
return { wsLink, server };
};
/**
* Standard WebSocket test setup for conversation WebSocket handler tests
*/
export const conversationWebSocketTestSetup = () =>
createWebSocketTestSetup("ws://localhost/events/socket");
@@ -0,0 +1,64 @@
import React from "react";
import { useConversationWebSocket } from "#/contexts/conversation-websocket-context";
import { useEventStore } from "#/stores/use-event-store";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { isV1Event } from "#/types/v1/type-guards";
import { OpenHandsEvent } from "#/types/v1/core";
/**
* Test component to access and display WebSocket connection state
*/
export function ConnectionStatusComponent() {
const { connectionState } = useConversationWebSocket();
return (
<div>
<div data-testid="connection-state">{connectionState}</div>
</div>
);
}
/**
* Test component to access and display event store values
*/
export function EventStoreComponent() {
const { events, uiEvents } = useEventStore();
return (
<div>
<div data-testid="events-count">{events.length}</div>
<div data-testid="ui-events-count">{uiEvents.length}</div>
<div data-testid="latest-event-id">
{isV1Event(events[events.length - 1])
? (events[events.length - 1] as OpenHandsEvent).id
: "none"}
</div>
</div>
);
}
/**
* Test component to access and display optimistic user message store values
*/
export function OptimisticUserMessageStoreComponent() {
const { optimisticUserMessage } = useOptimisticUserMessageStore();
return (
<div>
<div data-testid="optimistic-user-message">
{optimisticUserMessage || "none"}
</div>
</div>
);
}
/**
* Test component to access and display error message store values
*/
export function ErrorMessageStoreComponent() {
const { errorMessage } = useErrorMessageStore();
return (
<div>
<div data-testid="error-message">{errorMessage || "none"}</div>
</div>
);
}
+9 -32
View File
@@ -1,10 +1,7 @@
import { beforeAll, describe, expect, it, vi } from "vitest";
import { afterEach } from "node:test";
import { beforeAll, describe, expect, it, vi, afterEach } from "vitest";
import { useTerminal } from "#/hooks/use-terminal";
import { Command, useCommandStore } from "#/state/command-store";
import { AgentState } from "#/types/agent-state";
import { renderWithProviders } from "../../test-utils";
import { useAgentStore } from "#/stores/agent-store";
// Mock the WsClient context
vi.mock("#/context/ws-client-provider", () => ({
@@ -16,15 +13,7 @@ vi.mock("#/context/ws-client-provider", () => ({
}),
}));
interface TestTerminalComponentProps {
commands: Command[];
}
function TestTerminalComponent({ commands }: TestTerminalComponentProps) {
// Set commands in Zustand store
useCommandStore.setState({ commands });
// Set agent state in Zustand store
useAgentStore.setState({ curAgentState: AgentState.RUNNING });
function TestTerminalComponent() {
const ref = useTerminal();
return <div ref={ref} />;
}
@@ -57,10 +46,12 @@ describe("useTerminal", () => {
afterEach(() => {
vi.clearAllMocks();
// Reset command store between tests
useCommandStore.setState({ commands: [] });
});
it("should render", () => {
renderWithProviders(<TestTerminalComponent commands={[]} />);
renderWithProviders(<TestTerminalComponent />);
});
it("should render the commands in the terminal", () => {
@@ -69,26 +60,12 @@ describe("useTerminal", () => {
{ content: "hello", type: "output" },
];
renderWithProviders(<TestTerminalComponent commands={commands} />);
// Set commands in store before rendering to ensure they're picked up during initialization
useCommandStore.setState({ commands });
renderWithProviders(<TestTerminalComponent />);
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(1, "echo hello");
expect(mockTerminal.writeln).toHaveBeenNthCalledWith(2, "hello");
});
// This test is no longer relevant as secrets filtering has been removed
it.skip("should hide secrets in the terminal", () => {
const secret = "super_secret_github_token";
const anotherSecret = "super_secret_another_token";
const commands: Command[] = [
{
content: `export GITHUB_TOKEN=${secret},${anotherSecret},${secret}`,
type: "input",
},
{ content: secret, type: "output" },
];
renderWithProviders(<TestTerminalComponent commands={commands} />);
// This test is no longer relevant as secrets filtering has been removed
});
});
@@ -0,0 +1,316 @@
import { renderHook, waitFor } from "@testing-library/react";
import {
describe,
it,
expect,
beforeAll,
afterAll,
afterEach,
vi,
} from "vitest";
import { ws } from "msw";
import { setupServer } from "msw/node";
import { useWebSocket } from "#/hooks/use-websocket";
describe.skip("useWebSocket", () => {
// MSW WebSocket mock setup
const wsLink = ws.link("ws://acme.com/ws");
const mswServer = setupServer(
wsLink.addEventListener("connection", ({ client, server }) => {
// Establish the connection
server.connect();
// Send a welcome message to confirm connection
client.send("Welcome to the WebSocket!");
}),
);
beforeAll(() => mswServer.listen());
afterEach(() => mswServer.resetHandlers());
afterAll(() => mswServer.close());
it("should establish a WebSocket connection", async () => {
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
// Initially should not be connected
expect(result.current.isConnected).toBe(false);
expect(result.current.lastMessage).toBe(null);
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Should receive the welcome message from our mock
await waitFor(() => {
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
});
// Confirm that the WebSocket connection is established when the hook is used
expect(result.current.socket).toBeTruthy();
});
it("should handle incoming messages correctly", async () => {
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Should receive the welcome message from our mock
await waitFor(() => {
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
});
// Send another message from the mock server
wsLink.broadcast("Hello from server!");
await waitFor(() => {
expect(result.current.lastMessage).toBe("Hello from server!");
});
// Should have a messages array with all received messages
expect(result.current.messages).toEqual([
"Welcome to the WebSocket!",
"Hello from server!",
]);
});
it("should handle connection errors gracefully", async () => {
// Create a mock that will simulate an error
const errorLink = ws.link("ws://error-test.com/ws");
mswServer.use(
errorLink.addEventListener("connection", ({ client }) => {
// Simulate an error by closing the connection immediately
client.close(1006, "Connection failed");
}),
);
const { result } = renderHook(() => useWebSocket("ws://error-test.com/ws"));
// Initially should not be connected and no error
expect(result.current.isConnected).toBe(false);
expect(result.current.error).toBe(null);
// Wait for the connection to fail
await waitFor(() => {
expect(result.current.isConnected).toBe(false);
});
// Should have error information (the close event should trigger error state)
await waitFor(() => {
expect(result.current.error).not.toBe(null);
});
expect(result.current.error).toBeInstanceOf(Error);
// Should have meaningful error message (could be from onerror or onclose)
expect(
result.current.error?.message.includes("WebSocket closed with code 1006"),
).toBe(true);
// Should not crash the application
expect(result.current.socket).toBeTruthy();
});
it("should close the WebSocket connection on unmount", async () => {
const { result, unmount } = renderHook(() =>
useWebSocket("ws://acme.com/ws"),
);
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Verify connection is active
expect(result.current.isConnected).toBe(true);
expect(result.current.socket).toBeTruthy();
const closeSpy = vi.spyOn(result.current.socket!, "close");
// Unmount the component (this should trigger the useEffect cleanup)
unmount();
// Verify that WebSocket close was called during cleanup
expect(closeSpy).toHaveBeenCalledOnce();
});
it("should support query parameters in WebSocket URL", async () => {
const baseUrl = "ws://acme.com/ws";
const queryParams = {
token: "abc123",
userId: "user456",
version: "v1",
};
const { result } = renderHook(() => useWebSocket(baseUrl, { queryParams }));
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Verify that the WebSocket was created with query parameters
expect(result.current.socket).toBeTruthy();
expect(result.current.socket!.url).toBe(
"ws://acme.com/ws?token=abc123&userId=user456&version=v1",
);
});
it("should call onOpen handler when WebSocket connection opens", async () => {
const onOpenSpy = vi.fn();
const options = { onOpen: onOpenSpy };
const { result } = renderHook(() =>
useWebSocket("ws://acme.com/ws", options),
);
// Initially should not be connected
expect(result.current.isConnected).toBe(false);
expect(onOpenSpy).not.toHaveBeenCalled();
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// onOpen handler should have been called
expect(onOpenSpy).toHaveBeenCalledOnce();
});
it("should call onClose handler when WebSocket connection closes", async () => {
const onCloseSpy = vi.fn();
const options = { onClose: onCloseSpy };
const { result, unmount } = renderHook(() =>
useWebSocket("ws://acme.com/ws", options),
);
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
expect(onCloseSpy).not.toHaveBeenCalled();
// Unmount to trigger close
unmount();
// Wait for onClose handler to be called
await waitFor(() => {
expect(onCloseSpy).toHaveBeenCalledOnce();
});
});
it("should call onMessage handler when WebSocket receives a message", async () => {
const onMessageSpy = vi.fn();
const options = { onMessage: onMessageSpy };
const { result } = renderHook(() =>
useWebSocket("ws://acme.com/ws", options),
);
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Should receive the welcome message from our mock
await waitFor(() => {
expect(result.current.lastMessage).toBe("Welcome to the WebSocket!");
});
// onMessage handler should have been called for the welcome message
expect(onMessageSpy).toHaveBeenCalledOnce();
// Send another message from the mock server
wsLink.broadcast("Hello from server!");
await waitFor(() => {
expect(result.current.lastMessage).toBe("Hello from server!");
});
// onMessage handler should have been called twice now
expect(onMessageSpy).toHaveBeenCalledTimes(2);
});
it("should call onError handler when WebSocket encounters an error", async () => {
const onErrorSpy = vi.fn();
const options = { onError: onErrorSpy };
// Create a mock that will simulate an error
const errorLink = ws.link("ws://error-test.com/ws");
mswServer.use(
errorLink.addEventListener("connection", ({ client }) => {
// Simulate an error by closing the connection immediately
client.close(1006, "Connection failed");
}),
);
const { result } = renderHook(() =>
useWebSocket("ws://error-test.com/ws", options),
);
// Initially should not be connected and no error
expect(result.current.isConnected).toBe(false);
expect(onErrorSpy).not.toHaveBeenCalled();
// Wait for the connection to fail
await waitFor(() => {
expect(result.current.isConnected).toBe(false);
});
// Should have error information
await waitFor(() => {
expect(result.current.error).not.toBe(null);
});
// onError handler should have been called
expect(onErrorSpy).toHaveBeenCalledOnce();
});
it("should provide sendMessage function to send messages to WebSocket", async () => {
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
// Wait for connection to be established
await waitFor(() => {
expect(result.current.isConnected).toBe(true);
});
// Should have a sendMessage function
expect(result.current.sendMessage).toBeDefined();
expect(typeof result.current.sendMessage).toBe("function");
// Mock the WebSocket send method
const sendSpy = vi.spyOn(result.current.socket!, "send");
// Send a message
result.current.sendMessage("Hello WebSocket!");
// Verify that WebSocket.send was called with the correct message
expect(sendSpy).toHaveBeenCalledOnce();
expect(sendSpy).toHaveBeenCalledWith("Hello WebSocket!");
});
it("should not send message when WebSocket is not connected", () => {
const { result } = renderHook(() => useWebSocket("ws://acme.com/ws"));
// Initially should not be connected
expect(result.current.isConnected).toBe(false);
expect(result.current.sendMessage).toBeDefined();
// Mock the WebSocket send method (even though socket might be null)
const sendSpy = vi.fn();
if (result.current.socket) {
vi.spyOn(result.current.socket, "send").mockImplementation(sendSpy);
}
// Try to send a message when not connected
result.current.sendMessage("Hello WebSocket!");
// Verify that WebSocket.send was not called
expect(sendSpy).not.toHaveBeenCalled();
});
});
@@ -0,0 +1,130 @@
import { act, renderHook } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { useEventStore } from "#/stores/use-event-store";
import {
ActionEvent,
MessageEvent,
ObservationEvent,
SecurityRisk,
} from "#/types/v1/core";
const mockUserMessageEvent: MessageEvent = {
id: "test-event-1",
timestamp: Date.now().toString(),
source: "user",
llm_message: {
role: "user",
content: [{ type: "text", text: "Hello, world!" }],
},
activated_microagents: [],
extended_content: [],
};
const mockActionEvent: ActionEvent = {
id: "test-action-1",
timestamp: Date.now().toString(),
source: "agent",
thought: [{ type: "text", text: "I need to execute a bash command" }],
thinking_blocks: [],
action: {
kind: "ExecuteBashAction",
command: "echo hello",
is_input: false,
timeout: null,
reset: false,
},
tool_name: "execute_bash",
tool_call_id: "call_123",
tool_call: {
id: "call_123",
type: "function",
function: {
name: "execute_bash",
arguments: '{"command": "echo hello"}',
},
},
llm_response_id: "response_123",
security_risk: SecurityRisk.UNKNOWN,
};
const mockObservationEvent: ObservationEvent = {
id: "test-observation-1",
timestamp: Date.now().toString(),
source: "environment",
tool_name: "execute_bash",
tool_call_id: "call_123",
observation: {
kind: "ExecuteBashObservation",
output: "hello\n",
command: "echo hello",
exit_code: 0,
error: false,
timeout: false,
metadata: {
exit_code: 0,
pid: 12345,
username: "user",
hostname: "localhost",
working_dir: "/home/user",
py_interpreter_path: null,
prefix: "",
suffix: "",
},
},
action_id: "test-action-1",
};
describe("useEventStore", () => {
it("should render initial state correctly", () => {
const { result } = renderHook(() => useEventStore());
expect(result.current.events).toEqual([]);
});
it("should add an event to the store", () => {
const { result } = renderHook(() => useEventStore());
act(() => {
result.current.addEvent(mockUserMessageEvent);
});
expect(result.current.events).toEqual([mockUserMessageEvent]);
});
it("should retrieve events whose actions are replaced by their observations", () => {
const { result } = renderHook(() => useEventStore());
act(() => {
result.current.addEvent(mockUserMessageEvent);
result.current.addEvent(mockActionEvent);
result.current.addEvent(mockObservationEvent);
});
expect(result.current.uiEvents).toEqual([
mockUserMessageEvent,
mockObservationEvent,
]);
});
it("should clear all events when clearEvents is called", () => {
const { result } = renderHook(() => useEventStore());
// Add some events first
act(() => {
result.current.addEvent(mockUserMessageEvent);
result.current.addEvent(mockActionEvent);
});
// Verify events were added
expect(result.current.events).toHaveLength(2);
expect(result.current.uiEvents).toHaveLength(2);
// Clear events
act(() => {
result.current.clearEvents();
});
// Verify events were cleared
expect(result.current.events).toEqual([]);
expect(result.current.uiEvents).toEqual([]);
});
});
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import {
ActionEvent,
ObservationEvent,
MessageEvent,
SecurityRisk,
OpenHandsEvent,
} from "#/types/v1/core";
import { handleEventForUI } from "#/utils/handle-event-for-ui";
describe("handleEventForUI", () => {
const mockObservationEvent: ObservationEvent = {
id: "test-observation-1",
timestamp: Date.now().toString(),
source: "environment",
tool_name: "execute_bash",
tool_call_id: "call_123",
observation: {
kind: "ExecuteBashObservation",
output: "hello\n",
command: "echo hello",
exit_code: 0,
error: false,
timeout: false,
metadata: {
exit_code: 0,
pid: 12345,
username: "user",
hostname: "localhost",
working_dir: "/home/user",
py_interpreter_path: null,
prefix: "",
suffix: "",
},
},
action_id: "test-action-1",
};
const mockActionEvent: ActionEvent = {
id: "test-action-1",
timestamp: Date.now().toString(),
source: "agent",
thought: [{ type: "text", text: "I need to execute a bash command" }],
thinking_blocks: [],
action: {
kind: "ExecuteBashAction",
command: "echo hello",
is_input: false,
timeout: null,
reset: false,
},
tool_name: "execute_bash",
tool_call_id: "call_123",
tool_call: {
id: "call_123",
type: "function",
function: {
name: "execute_bash",
arguments: '{"command": "echo hello"}',
},
},
llm_response_id: "response_123",
security_risk: SecurityRisk.UNKNOWN,
};
const mockMessageEvent: MessageEvent = {
id: "test-event-1",
timestamp: Date.now().toString(),
source: "user",
llm_message: {
role: "user",
content: [{ type: "text", text: "Hello, world!" }],
},
activated_microagents: [],
extended_content: [],
};
it("should add non-observation events to the end of uiEvents", () => {
const initialUiEvents = [mockMessageEvent];
const result = handleEventForUI(mockActionEvent, initialUiEvents);
expect(result).toEqual([mockMessageEvent, mockActionEvent]);
expect(result).not.toBe(initialUiEvents); // Should return a new array
});
it("should replace corresponding action with observation when action exists", () => {
const initialUiEvents = [mockMessageEvent, mockActionEvent];
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
expect(result).toEqual([mockMessageEvent, mockObservationEvent]);
expect(result).not.toBe(initialUiEvents); // Should return a new array
});
it("should add observation to end when corresponding action is not found", () => {
const initialUiEvents = [mockMessageEvent];
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
expect(result).toEqual([mockMessageEvent, mockObservationEvent]);
expect(result).not.toBe(initialUiEvents); // Should return a new array
});
it("should handle empty uiEvents array", () => {
const initialUiEvents: OpenHandsEvent[] = [];
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
expect(result).toEqual([mockObservationEvent]);
expect(result).not.toBe(initialUiEvents); // Should return a new array
});
it("should not mutate the original uiEvents array", () => {
const initialUiEvents = [mockMessageEvent, mockActionEvent];
const originalLength = initialUiEvents.length;
const originalFirstEvent = initialUiEvents[0];
handleEventForUI(mockObservationEvent, initialUiEvents);
expect(initialUiEvents).toHaveLength(originalLength);
expect(initialUiEvents[0]).toBe(originalFirstEvent);
expect(initialUiEvents[1]).toBe(mockActionEvent); // Should not be replaced
});
it("should replace the correct action when multiple actions exist", () => {
const anotherActionEvent: ActionEvent = {
...mockActionEvent,
id: "test-action-2",
};
const initialUiEvents = [
mockMessageEvent,
mockActionEvent,
anotherActionEvent,
];
const result = handleEventForUI(mockObservationEvent, initialUiEvents);
expect(result).toEqual([
mockMessageEvent,
mockObservationEvent,
anotherActionEvent,
]);
});
});
@@ -24,4 +24,5 @@ test("mapProvider", () => {
expect(mapProvider("replicate")).toBe("Replicate");
expect(mapProvider("voyage")).toBe("Voyage AI");
expect(mapProvider("openrouter")).toBe("OpenRouter");
expect(mapProvider("clarifai")).toBe("Clarifai");
});
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.59.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.59.0",
"dependencies": {
"@heroui/react": "^2.8.4",
"@heroui/use-infinite-scroll": "^2.2.11",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.58.0",
"version": "0.59.0",
"private": true,
"type": "module",
"engines": {
@@ -121,7 +121,7 @@ class ConversationService {
reason?: string;
}>(url);
return data;
} catch (error) {
} catch {
// Error checking if feedback exists
return { exists: false };
}
@@ -7,7 +7,7 @@ import { TrajectoryActions } from "../trajectory/trajectory-actions";
import { createChatMessage } from "#/services/chat-service";
import { InteractiveChatBox } from "./interactive-chat-box";
import { AgentState } from "#/types/agent-state";
import { isOpenHandsAction } from "#/types/core/guards";
import { isOpenHandsAction, isActionOrObservation } from "#/types/core/guards";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { FeedbackModal } from "../feedback/feedback-modal";
import { useScrollToBottom } from "#/hooks/use-scroll-to-bottom";
@@ -24,6 +24,7 @@ import { LoadingSpinner } from "#/components/shared/loading-spinner";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useEventStore } from "#/stores/use-event-store";
import { ErrorMessageBanner } from "./error-message-banner";
import {
hasUserEvent,
@@ -34,6 +35,7 @@ import { useConfig } from "#/hooks/query/use-config";
import { validateFiles } from "#/utils/file-validation";
import { useConversationStore } from "#/state/conversation-store";
import ConfirmationModeEnabled from "./confirmation-mode-enabled";
import { isV0Event } from "#/types/v1/type-guards";
function getEntryPoint(
hasRepository: boolean | null,
@@ -47,7 +49,8 @@ function getEntryPoint(
export function ChatInterface() {
const { setMessageToSend } = useConversationStore();
const { errorMessage } = useErrorMessageStore();
const { send, isLoadingMessages, parsedEvents } = useWsClient();
const { send, isLoadingMessages } = useWsClient();
const storeEvents = useEventStore((state) => state.events);
const { setOptimisticUserMessage, getOptimisticUserMessage } =
useOptimisticUserMessageStore();
const { t } = useTranslation();
@@ -74,18 +77,24 @@ export function ChatInterface() {
const optimisticUserMessage = getOptimisticUserMessage();
const events = parsedEvents.filter(shouldRenderEvent);
const events = storeEvents
.filter(isV0Event)
.filter(isActionOrObservation)
.filter(shouldRenderEvent);
// Check if there are any substantive agent actions (not just system messages)
const hasSubstantiveAgentActions = React.useMemo(
() =>
parsedEvents.some(
(event) =>
isOpenHandsAction(event) &&
event.source === "agent" &&
event.action !== "system",
),
[parsedEvents],
storeEvents
.filter(isV0Event)
.filter(isActionOrObservation)
.some(
(event) =>
isOpenHandsAction(event) &&
event.source === "agent" &&
event.action !== "system",
),
[storeEvents],
);
const handleSendMessage = async (
@@ -120,7 +120,7 @@ export function InteractiveChatBox({
// Step 5: Handle failed results
handleFailedFiles(fileResults, imageResults);
} catch (error) {
} catch {
// Clear loading states and show error
clearLoadingStates(validFiles, validImages);
displayErrorToast("An unexpected error occurred while processing files");
@@ -90,7 +90,7 @@ export function ConversationCard({
}
}
// VS Code URL not available
} catch (error) {
} catch {
// Failed to fetch VS Code URL
}
}
@@ -23,7 +23,7 @@ export function useUrlSearch(inputValue: string, provider: Provider) {
);
setUrlSearchResults(repositories);
} catch (error) {
} catch {
setUrlSearchResults([]);
} finally {
setIsUrlSearchLoading(false);
@@ -28,7 +28,7 @@ export function CancelSubscriptionModal({
await cancelSubscriptionMutation.mutateAsync();
displaySuccessToast(t(I18nKey.PAYMENT$SUBSCRIPTION_CANCELLED));
onClose();
} catch (error) {
} catch {
displayErrorToast(t(I18nKey.ERROR$GENERIC));
}
};
@@ -39,7 +39,7 @@ export function CreateApiKeyModal({
onKeyCreated(newKey);
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_CREATED));
setNewKeyName("");
} catch (error) {
} catch {
displayErrorToast(t(I18nKey.ERROR$GENERIC));
}
};
@@ -32,7 +32,7 @@ export function DeleteApiKeyModal({
await deleteApiKeyMutation.mutateAsync(keyToDelete.id);
displaySuccessToast(t(I18nKey.SETTINGS$API_KEY_DELETED));
onClose();
} catch (error) {
} catch {
displayErrorToast(t(I18nKey.ERROR$GENERIC));
}
};
@@ -2,7 +2,7 @@ import React from "react";
import { useLocation } from "react-router";
import { useGitUser } from "#/hooks/query/use-git-user";
import { UserActions } from "./user-actions";
import { AllHandsLogoButton } from "#/components/shared/buttons/all-hands-logo-button";
import { OpenHandsLogoButton } from "#/components/shared/buttons/openhands-logo-button";
import { NewProjectButton } from "#/components/shared/buttons/new-project-button";
import { ConversationPanelButton } from "#/components/shared/buttons/conversation-panel-button";
import { SettingsModal } from "#/components/shared/modals/settings/settings-modal";
@@ -74,7 +74,7 @@ export function Sidebar() {
<nav className="flex flex-row md:flex-col items-center justify-between w-full h-auto md:w-auto md:h-full">
<div className="flex flex-row md:flex-col items-center gap-[26px]">
<div className="flex items-center justify-center">
<AllHandsLogoButton />
<OpenHandsLogoButton />
</div>
<div>
<NewProjectButton disabled={settings?.EMAIL_VERIFIED === false} />
@@ -5,11 +5,13 @@ import { AgentState } from "#/types/agent-state";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { useWsClient } from "#/context/ws-client-provider";
import { ActionTooltip } from "../action-tooltip";
import { isOpenHandsAction } from "#/types/core/guards";
import { isOpenHandsAction, isActionOrObservation } from "#/types/core/guards";
import { ActionSecurityRisk } from "#/stores/security-analyzer-store";
import { RiskAlert } from "#/components/shared/risk-alert";
import WarningIcon from "#/icons/u-warning.svg?react";
import { useEventMessageStore } from "#/stores/event-message-store";
import { useEventStore } from "#/stores/use-event-store";
import { isV0Event } from "#/types/v1/type-guards";
export function ConfirmationButtons() {
const submittedEventIds = useEventMessageStore(
@@ -21,10 +23,13 @@ export function ConfirmationButtons() {
const { t } = useTranslation();
const { send, parsedEvents } = useWsClient();
const { send } = useWsClient();
const events = useEventStore((state) => state.events);
// Find the most recent action awaiting confirmation
const awaitingAction = parsedEvents
const awaitingAction = events
.filter(isV0Event)
.filter(isActionOrObservation)
.slice()
.reverse()
.find((ev) => {
@@ -3,13 +3,13 @@ import AllHandsLogo from "#/assets/branding/all-hands-logo.svg?react";
import { I18nKey } from "#/i18n/declaration";
import { TooltipButton } from "./tooltip-button";
export function AllHandsLogoButton() {
export function OpenHandsLogoButton() {
const { t } = useTranslation();
return (
<TooltipButton
tooltip={t(I18nKey.BRANDING$ALL_HANDS_AI)}
ariaLabel={t(I18nKey.BRANDING$ALL_HANDS_LOGO)}
tooltip={t(I18nKey.BRANDING$OPENHANDS)}
ariaLabel={t(I18nKey.BRANDING$OPENHANDS_LOGO)}
navLinkTo="/"
>
<AllHandsLogo width={46} height={30} />
@@ -20,7 +20,7 @@ export function ModalBackdrop({ children, onClose }: ModalBackdropProps) {
};
return (
<div className="fixed inset-0 flex items-center justify-center z-20">
<div className="fixed inset-0 flex items-center justify-center z-60">
<div
onClick={handleClick}
className="fixed inset-0 bg-black opacity-60"
@@ -275,7 +275,7 @@ export function ConversationSubscriptionsProvider({
setActiveConversationIds((prev) =>
prev.includes(conversationId) ? prev : [...prev, conversationId],
);
} catch (error) {
} catch {
// Clean up the event handler if there was an error
delete eventHandlersRef.current[conversationId];
}
+5 -23
View File
@@ -11,13 +11,11 @@ import {
CommandAction,
FileEditAction,
FileWriteAction,
OpenHandsAction,
UserMessageAction,
} from "#/types/core/actions";
import { Conversation } from "#/api/open-hands.types";
import { useUserProviders } from "#/hooks/use-user-providers";
import { useActiveConversation } from "#/hooks/query/use-active-conversation";
import { OpenHandsObservation } from "#/types/core/observations";
import {
isAgentStateChangeObservation,
isErrorObservation,
@@ -28,6 +26,7 @@ import {
} from "#/types/core/guards";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import { useEventStore } from "#/stores/use-event-store";
export type WebSocketStatus = "CONNECTING" | "CONNECTED" | "DISCONNECTED";
@@ -72,16 +71,12 @@ const isMessageAction = (
interface UseWsClient {
webSocketStatus: WebSocketStatus;
isLoadingMessages: boolean;
events: Record<string, unknown>[];
parsedEvents: (OpenHandsAction | OpenHandsObservation)[];
send: (event: Record<string, unknown>) => void;
}
const WsClientContext = React.createContext<UseWsClient>({
webSocketStatus: "DISCONNECTED",
isLoadingMessages: true,
events: [],
parsedEvents: [],
send: () => {
throw new Error("not connected");
},
@@ -133,14 +128,11 @@ export function WsClientProvider({
}: React.PropsWithChildren<WsClientProviderProps>) {
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
const { addEvent, clearEvents } = useEventStore();
const queryClient = useQueryClient();
const sioRef = React.useRef<Socket | null>(null);
const [webSocketStatus, setWebSocketStatus] =
React.useState<WebSocketStatus>("DISCONNECTED");
const [events, setEvents] = React.useState<Record<string, unknown>[]>([]);
const [parsedEvents, setParsedEvents] = React.useState<
(OpenHandsAction | OpenHandsObservation)[]
>([]);
const lastEventRef = React.useRef<Record<string, unknown> | null>(null);
const { providers } = useUserProviders();
@@ -188,7 +180,7 @@ export function WsClientProvider({
}
if (isOpenHandsAction(event) || isOpenHandsObservation(event)) {
setParsedEvents((prevEvents) => [...prevEvents, event]);
addEvent(event); // Event is already OpenHandsParsedEvent
}
if (isErrorObservation(event)) {
@@ -249,7 +241,6 @@ export function WsClientProvider({
}
}
setEvents((prevEvents) => [...prevEvents, event]);
if (!Number.isNaN(parseInt(event.id as string, 10))) {
lastEventRef.current = event;
}
@@ -286,9 +277,7 @@ export function WsClientProvider({
React.useEffect(() => {
lastEventRef.current = null;
// reset events when conversationId changes
setEvents([]);
setParsedEvents([]);
clearEvents();
setWebSocketStatus("CONNECTING");
}, [conversationId]);
@@ -397,16 +386,9 @@ export function WsClientProvider({
() => ({
webSocketStatus,
isLoadingMessages: messageRateHandler.isUnderThreshold,
events,
parsedEvents,
send,
}),
[
webSocketStatus,
messageRateHandler.isUnderThreshold,
events,
parsedEvents,
],
[webSocketStatus, messageRateHandler.isUnderThreshold],
);
return <WsClientContext value={value}>{children}</WsClientContext>;
@@ -0,0 +1,156 @@
import React, {
createContext,
useContext,
useEffect,
useState,
useCallback,
useMemo,
} from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useWebSocket } from "#/hooks/use-websocket";
import { useEventStore } from "#/stores/use-event-store";
import { useErrorMessageStore } from "#/stores/error-message-store";
import { useOptimisticUserMessageStore } from "#/stores/optimistic-user-message-store";
import {
isV1Event,
isAgentErrorEvent,
isUserMessageEvent,
isActionEvent,
} from "#/types/v1/type-guards";
import { handleActionEventCacheInvalidation } from "#/utils/cache-utils";
interface ConversationWebSocketContextType {
connectionState: "CONNECTING" | "OPEN" | "CLOSED" | "CLOSING";
}
const ConversationWebSocketContext = createContext<
ConversationWebSocketContextType | undefined
>(undefined);
export function ConversationWebSocketProvider({
children,
conversationId,
}: {
children: React.ReactNode;
conversationId?: string;
}) {
const [connectionState, setConnectionState] = useState<
"CONNECTING" | "OPEN" | "CLOSED" | "CLOSING"
>("CONNECTING");
const queryClient = useQueryClient();
const { addEvent } = useEventStore();
const { setErrorMessage, removeErrorMessage } = useErrorMessageStore();
const { removeOptimisticUserMessage } = useOptimisticUserMessageStore();
const handleMessage = useCallback(
(messageEvent: MessageEvent) => {
try {
const event = JSON.parse(messageEvent.data);
// Use type guard to validate v1 event structure
if (isV1Event(event)) {
addEvent(event);
// Handle AgentErrorEvent specifically
if (isAgentErrorEvent(event)) {
setErrorMessage(event.error);
}
// Clear optimistic user message when a user message is confirmed
if (isUserMessageEvent(event)) {
removeOptimisticUserMessage();
}
// Handle cache invalidation for ActionEvent
if (isActionEvent(event)) {
const currentConversationId =
conversationId || "test-conversation-id"; // TODO: Get from context
handleActionEventCacheInvalidation(
event,
currentConversationId,
queryClient,
);
}
}
} catch (error) {
// eslint-disable-next-line no-console
console.warn("Failed to parse WebSocket message as JSON:", error);
}
},
[addEvent, setErrorMessage, removeOptimisticUserMessage, queryClient],
);
const websocketOptions = useMemo(
() => ({
onOpen: () => {
setConnectionState("OPEN");
removeErrorMessage(); // Clear any previous error messages on successful connection
},
onClose: (event: CloseEvent) => {
setConnectionState("CLOSED");
// Set error message for unexpected disconnects (not normal closure)
if (event.code !== 1000) {
setErrorMessage(
`Connection lost: ${event.reason || "Unexpected disconnect"}`,
);
}
},
onError: () => {
setConnectionState("CLOSED");
setErrorMessage("Failed to connect to server");
},
onMessage: handleMessage,
}),
[handleMessage, setErrorMessage, removeErrorMessage],
);
const { socket } = useWebSocket(
"ws://localhost/events/socket",
websocketOptions,
);
useEffect(() => {
if (socket) {
// Update state based on socket readyState
const updateState = () => {
switch (socket.readyState) {
case WebSocket.CONNECTING:
setConnectionState("CONNECTING");
break;
case WebSocket.OPEN:
setConnectionState("OPEN");
break;
case WebSocket.CLOSING:
setConnectionState("CLOSING");
break;
case WebSocket.CLOSED:
setConnectionState("CLOSED");
break;
default:
setConnectionState("CLOSED");
break;
}
};
updateState();
}
}, [socket]);
const contextValue = useMemo(() => ({ connectionState }), [connectionState]);
return (
<ConversationWebSocketContext.Provider value={contextValue}>
{children}
</ConversationWebSocketContext.Provider>
);
}
export const useConversationWebSocket =
(): ConversationWebSocketContextType => {
const context = useContext(ConversationWebSocketContext);
if (context === undefined) {
throw new Error(
"useConversationWebSocket must be used within a ConversationWebSocketProvider",
);
}
return context;
};
@@ -0,0 +1,55 @@
import React from "react";
import { WsClientProvider } from "#/context/ws-client-provider";
import { ConversationWebSocketProvider } from "#/contexts/conversation-websocket-context";
interface WebSocketProviderWrapperProps {
children: React.ReactNode;
conversationId: string;
version: 0 | 1;
}
/**
* A wrapper component that conditionally renders either the old v0 WebSocket provider
* or the new v1 WebSocket provider based on the version prop.
*
* @param version - 0 for old WsClientProvider, 1 for new ConversationWebSocketProvider
* @param conversationId - The conversation ID to pass to the provider
* @param children - The child components to wrap
*
* @example
* // Use the old v0 provider
* <WebSocketProviderWrapper version={0} conversationId="conv-123">
* <ChatComponent />
* </WebSocketProviderWrapper>
*
* @example
* // Use the new v1 provider
* <WebSocketProviderWrapper version={1} conversationId="conv-123">
* <ChatComponent />
* </WebSocketProviderWrapper>
*/
export function WebSocketProviderWrapper({
children,
conversationId,
version,
}: WebSocketProviderWrapperProps) {
if (version === 0) {
return (
<WsClientProvider conversationId={conversationId}>
{children}
</WsClientProvider>
);
}
if (version === 1) {
return (
<ConversationWebSocketProvider conversationId={conversationId}>
{children}
</ConversationWebSocketProvider>
);
}
throw new Error(
`Unsupported WebSocket provider version: ${version}. Supported versions are 0 and 1.`,
);
}
+1 -1
View File
@@ -25,7 +25,7 @@ function PosthogInit() {
try {
const config = await OptionService.getConfig();
setPosthogClientKey(config.POSTHOG_CLIENT_KEY);
} catch (error) {
} catch {
displayErrorToast("Error fetching PostHog client key");
}
})();
@@ -2,10 +2,9 @@ import { useTranslation } from "react-i18next";
import React from "react";
import posthog from "posthog-js";
import { useParams, useNavigate } from "react-router";
import { useWsClient } from "#/context/ws-client-provider";
import { transformVSCodeUrl } from "#/utils/vscode-url-helper";
import useMetricsStore from "#/stores/metrics-store";
import { isSystemMessage } from "#/types/core/guards";
import { isSystemMessage, isActionOrObservation } from "#/types/core/guards";
import { ConversationStatus } from "#/types/conversation-status";
import ConversationService from "#/api/conversation-service/conversation-service.api";
import { useDeleteConversation } from "./mutation/use-delete-conversation";
@@ -14,6 +13,8 @@ import { useGetTrajectory } from "./mutation/use-get-trajectory";
import { downloadTrajectory } from "#/utils/download-trajectory";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { I18nKey } from "#/i18n/declaration";
import { useEventStore } from "#/stores/use-event-store";
import { isV0Event } from "#/types/v1/type-guards";
interface UseConversationNameContextMenuProps {
conversationId?: string;
@@ -31,7 +32,7 @@ export function useConversationNameContextMenu({
const { t } = useTranslation();
const { conversationId: currentConversationId } = useParams();
const navigate = useNavigate();
const { parsedEvents } = useWsClient();
const events = useEventStore((state) => state.events);
const { mutate: deleteConversation } = useDeleteConversation();
const { mutate: stopConversation } = useStopConversation();
const { mutate: getTrajectory } = useGetTrajectory();
@@ -46,7 +47,10 @@ export function useConversationNameContextMenu({
const [confirmStopModalVisible, setConfirmStopModalVisible] =
React.useState(false);
const systemMessage = parsedEvents.find(isSystemMessage);
const systemMessage = events
.filter(isV0Event)
.filter(isActionOrObservation)
.find(isSystemMessage);
const handleDelete = (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
@@ -136,7 +140,7 @@ export function useConversationNameContextMenu({
}
}
// VS Code URL not available
} catch (error) {
} catch {
// Failed to fetch VS Code URL
}
}
+4 -2
View File
@@ -3,6 +3,7 @@ import { useWsClient } from "#/context/ws-client-provider";
import { generateAgentStateChangeEvent } from "#/services/agent-state-service";
import { AgentState } from "#/types/agent-state";
import { displayErrorToast } from "#/utils/custom-toast-handlers";
import { useEventStore } from "#/stores/use-event-store";
interface ServerError {
error: boolean | string;
@@ -13,7 +14,8 @@ interface ServerError {
const isServerError = (data: object): data is ServerError => "error" in data;
export const useHandleWSEvents = () => {
const { events, send } = useWsClient();
const { send } = useWsClient();
const events = useEventStore((state) => state.events);
React.useEffect(() => {
if (!events.length) {
@@ -35,7 +37,7 @@ export const useHandleWSEvents = () => {
return;
}
if (event.type === "error") {
if ("type" in event && event.type === "error") {
const message: string = `${event.message}`;
if (message.startsWith("Agent reached maximum")) {
// We set the agent state to paused here - if the user clicks resume, it auto updates the max iterations
+9 -3
View File
@@ -3,6 +3,7 @@ import { useConfig } from "./query/use-config";
import { useGitUser } from "./query/use-git-user";
import { getLoginMethod, LoginMethod } from "#/utils/local-storage";
import reoService, { ReoIdentity } from "#/utils/reo";
import { isProductionDomain } from "#/utils/utils";
/**
* Maps login method to Reo identity type
@@ -92,10 +93,14 @@ export const useReoTracking = () => {
const { data: user } = useGitUser();
const [hasIdentified, setHasIdentified] = React.useState(false);
// Initialize Reo.dev when in SaaS mode
// Initialize Reo.dev when in SaaS mode and on the correct domain
React.useEffect(() => {
const initReo = async () => {
if (config?.APP_MODE === "saas" && !reoService.isInitialized()) {
if (
config?.APP_MODE === "saas" &&
isProductionDomain() &&
!reoService.isInitialized()
) {
await reoService.init();
}
};
@@ -103,10 +108,11 @@ export const useReoTracking = () => {
initReo();
}, [config?.APP_MODE]);
// Identify user when user data is available and we're in SaaS mode
// Identify user when user data is available and we're in SaaS mode on correct domain
React.useEffect(() => {
if (
config?.APP_MODE !== "saas" ||
!isProductionDomain() ||
!user ||
hasIdentified ||
!reoService.isInitialized()
+85
View File
@@ -0,0 +1,85 @@
import React from "react";
export const useWebSocket = <T = string>(
url: string,
options?: {
queryParams?: Record<string, string>;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onMessage?: (event: MessageEvent) => void;
onError?: (event: Event) => void;
},
) => {
const [isConnected, setIsConnected] = React.useState(false);
const [lastMessage, setLastMessage] = React.useState<T | null>(null);
const [messages, setMessages] = React.useState<T[]>([]);
const [error, setError] = React.useState<Error | null>(null);
const wsRef = React.useRef<WebSocket | null>(null);
React.useEffect(() => {
// Build URL with query parameters if provided
let wsUrl = url;
if (options?.queryParams) {
const params = new URLSearchParams(options.queryParams);
wsUrl = `${url}?${params.toString()}`;
}
const ws = new WebSocket(wsUrl);
wsRef.current = ws;
ws.onopen = (event) => {
setIsConnected(true);
setError(null); // Clear any previous errors
options?.onOpen?.(event);
};
ws.onmessage = (event) => {
setLastMessage(event.data);
setMessages((prev) => [...prev, event.data]);
options?.onMessage?.(event);
};
ws.onclose = (event) => {
setIsConnected(false);
// If the connection closes with an error code, treat it as an error
if (event.code !== 1000) {
// 1000 is normal closure
setError(
new Error(
`WebSocket closed with code ${event.code}: ${event.reason || "Connection closed unexpectedly"}`,
),
);
// Also call onError handler for error closures
options?.onError?.(event);
}
options?.onClose?.(event);
};
ws.onerror = (event) => {
setIsConnected(false);
options?.onError?.(event);
};
return () => {
ws.close();
};
}, [url, options]);
const sendMessage = React.useCallback(
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
if (wsRef.current && wsRef.current.readyState === WebSocket.OPEN) {
wsRef.current.send(data);
}
},
[],
);
return {
isConnected,
lastMessage,
messages,
error,
socket: wsRef.current,
sendMessage,
};
};
+2 -2
View File
@@ -168,8 +168,8 @@ export enum I18nKey {
GITHUB$CODE_NOT_IN_GITHUB = "GITHUB$CODE_NOT_IN_GITHUB",
GITHUB$START_FROM_SCRATCH = "GITHUB$START_FROM_SCRATCH",
AVATAR$ALT_TEXT = "AVATAR$ALT_TEXT",
BRANDING$ALL_HANDS_AI = "BRANDING$ALL_HANDS_AI",
BRANDING$ALL_HANDS_LOGO = "BRANDING$ALL_HANDS_LOGO",
BRANDING$OPENHANDS = "BRANDING$OPENHANDS",
BRANDING$OPENHANDS_LOGO = "BRANDING$OPENHANDS_LOGO",
ERROR$GENERIC = "ERROR$GENERIC",
GITHUB$AUTH_SCOPE = "GITHUB$AUTH_SCOPE",
FILE_SERVICE$INVALID_FILE_PATH = "FILE_SERVICE$INVALID_FILE_PATH",
+30 -30
View File
@@ -2687,37 +2687,37 @@
"tr": "Kullanıcı avatarı",
"uk": "аватар користувача"
},
"BRANDING$ALL_HANDS_AI": {
"en": "All Hands AI",
"ja": "All Hands AI",
"zh-CN": "All Hands AI",
"zh-TW": "All Hands AI",
"ko-KR": "All Hands AI",
"de": "All Hands AI",
"no": "All Hands AI",
"it": "All Hands AI",
"pt": "All Hands AI",
"es": "All Hands AI",
"ar": "All Hands AI",
"fr": "All Hands AI",
"tr": "All Hands AI",
"uk": "All Hands AI"
"BRANDING$OPENHANDS": {
"en": "OpenHands",
"ja": "OpenHands",
"zh-CN": "OpenHands",
"zh-TW": "OpenHands",
"ko-KR": "OpenHands",
"de": "OpenHands",
"no": "OpenHands",
"it": "OpenHands",
"pt": "OpenHands",
"es": "OpenHands",
"ar": "OpenHands",
"fr": "OpenHands",
"tr": "OpenHands",
"uk": "OpenHands"
},
"BRANDING$ALL_HANDS_LOGO": {
"en": "All Hands Logo",
"ja": "All Handsロゴ",
"zh-CN": "All Hands标志",
"zh-TW": "All Hands標誌",
"ko-KR": "All Hands 로고",
"de": "All Hands Logo",
"no": "All Hands Logo",
"it": "Logo All Hands",
"pt": "Logo All Hands",
"es": "Logo de All Hands",
"ar": "شعار All Hands",
"fr": "Logo All Hands",
"tr": "All Hands Logosu",
"uk": "All Hands лого"
"BRANDING$OPENHANDS_LOGO": {
"en": "OpenHands Logo",
"ja": "OpenHandsロゴ",
"zh-CN": "OpenHands标志",
"zh-TW": "OpenHands標誌",
"ko-KR": "OpenHands 로고",
"de": "OpenHands Logo",
"no": "OpenHands Logo",
"it": "Logo OpenHands",
"pt": "Logo OpenHands",
"es": "Logo de OpenHands",
"ar": "شعار OpenHands",
"fr": "Logo OpenHands",
"tr": "OpenHands Logosu",
"uk": "OpenHands лого"
},
"ERROR$GENERIC": {
"en": "An error occurred",
+55
View File
@@ -5,6 +5,8 @@ import {
UserMessageAction,
} from "#/types/core/actions";
import { AgentStateChangeObservation } from "#/types/core/observations";
import { MessageEvent } from "#/types/v1/core";
import { AgentErrorEvent } from "#/types/v1/core/events/observation-event";
import { MockSessionMessaage } from "./session-history.mock";
export const generateAgentStateChangeObservation = (
@@ -73,3 +75,56 @@ export const emitMessages = (
}
});
};
// V1 Event Mock Factories for WebSocket Testing
/**
* Creates a mock MessageEvent for testing WebSocket event handling
*/
export const createMockMessageEvent = (
overrides: Partial<MessageEvent> = {},
): MessageEvent => ({
id: "test-event-123",
timestamp: new Date().toISOString(),
source: "agent",
llm_message: {
role: "assistant",
content: [{ type: "text", text: "Hello from agent" }],
},
activated_microagents: [],
extended_content: [],
...overrides,
});
/**
* Creates a mock user MessageEvent for testing WebSocket event handling
*/
export const createMockUserMessageEvent = (
overrides: Partial<MessageEvent> = {},
): MessageEvent => ({
id: "user-message-123",
timestamp: new Date().toISOString(),
source: "user",
llm_message: {
role: "user",
content: [{ type: "text", text: "Hello from user" }],
},
activated_microagents: [],
extended_content: [],
...overrides,
});
/**
* Creates a mock AgentErrorEvent for testing error handling
*/
export const createMockAgentErrorEvent = (
overrides: Partial<AgentErrorEvent> = {},
): AgentErrorEvent => ({
id: "error-event-123",
timestamp: new Date().toISOString(),
source: "agent",
tool_name: "str_replace_editor",
tool_call_id: "tool-call-456",
error: "Failed to execute command: Permission denied",
...overrides,
});
+3 -3
View File
@@ -11,7 +11,6 @@ import { useAgentStore } from "#/stores/agent-store";
import { AgentState } from "#/types/agent-state";
import { useBatchFeedback } from "#/hooks/query/use-batch-feedback";
import { WsClientProvider } from "#/context/ws-client-provider";
import { EventHandler } from "../wrapper/event-handler";
import { useConversationConfig } from "#/hooks/query/use-conversation-config";
@@ -28,6 +27,7 @@ import { ConversationName } from "#/components/features/conversation/conversatio
import { ConversationTabs } from "#/components/features/conversation/conversation-tabs/conversation-tabs";
import { useStartConversation } from "#/hooks/mutation/use-start-conversation";
import { WebSocketProviderWrapper } from "#/contexts/websocket-provider-wrapper";
function AppContent() {
useConversationConfig();
@@ -106,7 +106,7 @@ function AppContent() {
});
return (
<WsClientProvider conversationId={conversationId}>
<WebSocketProviderWrapper version={0} conversationId={conversationId}>
<ConversationSubscriptionsProvider>
<EventHandler>
<div
@@ -122,7 +122,7 @@ function AppContent() {
</div>
</EventHandler>
</ConversationSubscriptionsProvider>
</WsClientProvider>
</WebSocketProviderWrapper>
);
}
+38
View File
@@ -0,0 +1,38 @@
import { create } from "zustand";
import { OpenHandsEvent } from "#/types/v1/core";
import { handleEventForUI } from "#/utils/handle-event-for-ui";
import { OpenHandsParsedEvent } from "#/types/core";
import { isV1Event } from "#/types/v1/type-guards";
// While we transition to v1 events, our store can handle both v0 and v1 events
type OHEvent = OpenHandsEvent | OpenHandsParsedEvent;
interface EventState {
events: OHEvent[];
uiEvents: OHEvent[];
addEvent: (event: OHEvent) => void;
clearEvents: () => void;
}
export const useEventStore = create<EventState>()((set) => ({
events: [],
uiEvents: [],
addEvent: (event: OHEvent) =>
set((state) => {
const newEvents = [...state.events, event];
const newUiEvents = isV1Event(event)
? // @ts-expect-error - temporary, needs proper typing
handleEventForUI(event, state.uiEvents)
: [...state.uiEvents, event];
return {
events: newEvents,
uiEvents: newUiEvents,
};
}),
clearEvents: () =>
set(() => ({
events: [],
uiEvents: [],
})),
}));
+5
View File
@@ -105,3 +105,8 @@ export const isStatusUpdate = (event: unknown): event is StatusUpdate =>
"status_update" in event &&
"type" in event &&
"id" in event;
export const isActionOrObservation = (
event: OpenHandsParsedEvent,
): event is OpenHandsAction | OpenHandsObservation =>
isOpenHandsAction(event) || isOpenHandsObservation(event);
+3
View File
@@ -2,6 +2,9 @@ import { OpenHandsAction } from "./actions";
import { OpenHandsObservation } from "./observations";
import { OpenHandsVariance } from "./variances";
/**
* @deprecated Will be removed once we fully transition to v1 events
*/
export type OpenHandsParsedEvent =
| OpenHandsAction
| OpenHandsObservation
+115
View File
@@ -0,0 +1,115 @@
import { OpenHandsEvent, ObservationEvent, BaseEvent } from "./core";
import { AgentErrorEvent } from "./core/events/observation-event";
import { MessageEvent } from "./core/events/message-event";
import { ActionEvent } from "./core/events/action-event";
import type { OpenHandsParsedEvent } from "../core/index";
/**
* Type guard to check if an unknown value is a valid BaseEvent
* @param value - The value to check
* @returns true if the value is a valid BaseEvent
*/
export function isBaseEvent(value: unknown): value is BaseEvent {
return (
value !== null &&
typeof value === "object" &&
"id" in value &&
"timestamp" in value &&
"source" in value &&
typeof value.id === "string" &&
value.id.length > 0 &&
typeof value.timestamp === "string" &&
value.timestamp.length > 0 &&
typeof value.source === "string" &&
(value.source === "agent" ||
value.source === "user" ||
value.source === "environment")
);
}
/**
* Type guard function to check if an event is an observation event
*/
export const isObservationEvent = (
event: OpenHandsEvent,
): event is ObservationEvent =>
event.source === "environment" &&
"action_id" in event &&
"observation" in event;
/**
* Type guard function to check if an event is an agent error event
*/
export const isAgentErrorEvent = (
event: OpenHandsEvent,
): event is AgentErrorEvent =>
event.source === "agent" &&
"tool_name" in event &&
"tool_call_id" in event &&
"error" in event &&
typeof event.tool_name === "string" &&
typeof event.tool_call_id === "string" &&
typeof event.error === "string";
/**
* Type guard function to check if an event is a user message event
*/
export const isUserMessageEvent = (
event: OpenHandsEvent,
): event is MessageEvent =>
"llm_message" in event &&
typeof event.llm_message === "object" &&
event.llm_message !== null &&
"role" in event.llm_message &&
event.llm_message.role === "user";
/**
* Type guard function to check if an event is an action event
*/
export const isActionEvent = (event: OpenHandsEvent): event is ActionEvent =>
event.source === "agent" &&
"action" in event &&
"tool_name" in event &&
"tool_call_id" in event &&
typeof event.tool_name === "string" &&
typeof event.tool_call_id === "string";
// =============================================================================
// TEMPORARY COMPATIBILITY TYPE GUARDS
// These will be removed once we fully migrate to V1 events
// =============================================================================
/**
* TEMPORARY: Type guard to check if an event is a V1 OpenHandsEvent
* Uses isBaseEvent to validate the complete event structure
*
* @deprecated This is temporary until full V1 migration is complete
*/
export function isV1Event(
event: OpenHandsEvent | OpenHandsParsedEvent,
): event is OpenHandsEvent {
// Use isBaseEvent to validate the complete BaseEvent structure
// This ensures the event has all required properties with correct types
return isBaseEvent(event);
}
/**
* TEMPORARY: Type guard to check if an event is a V0 OpenHandsParsedEvent
*
* @deprecated This is temporary until full V1 migration is complete
*/
export function isV0Event(
event: OpenHandsEvent | OpenHandsParsedEvent,
): event is OpenHandsParsedEvent {
// Handle null/undefined cases
if (!event || typeof event !== "object") {
return false;
}
// V0 events have numeric IDs and either 'action' or 'observation' properties
return (
"id" in event &&
typeof event.id === "number" &&
("action" in event || "observation" in event)
);
}
+44
View File
@@ -0,0 +1,44 @@
import { QueryClient } from "@tanstack/react-query";
import type { ActionEvent } from "#/types/v1/core/events/action-event";
import { stripWorkspacePrefix } from "./path-utils";
/**
* Cache invalidation utilities for TanStack Query
*/
/**
* Handle cache invalidation for ActionEvent
* Invalidates relevant query caches based on the action type
*
* @param event - The ActionEvent to process
* @param conversationId - The conversation ID for cache keys
* @param queryClient - The TanStack Query client instance
*/
export const handleActionEventCacheInvalidation = (
event: ActionEvent,
conversationId: string,
queryClient: QueryClient,
) => {
const { action } = event;
// Invalidate file_changes cache for file-related actions
if (
action.kind === "StrReplaceEditorAction" ||
action.kind === "ExecuteBashAction"
) {
queryClient.invalidateQueries(
{
queryKey: ["file_changes", conversationId],
},
{ cancelRefetch: false },
);
}
// Invalidate specific file diff cache for file modifications
if (action.kind === "StrReplaceEditorAction" && action.path) {
const strippedPath = stripWorkspacePrefix(action.path);
queryClient.invalidateQueries({
queryKey: ["file_diff", conversationId, strippedPath],
});
}
};
+31
View File
@@ -0,0 +1,31 @@
import { OpenHandsEvent } from "#/types/v1/core";
import { isObservationEvent } from "#/types/v1/type-guards";
/**
* Handles adding an event to the UI events array, with special logic for observation events
*/
export const handleEventForUI = (
event: OpenHandsEvent,
uiEvents: OpenHandsEvent[],
): OpenHandsEvent[] => {
const newUiEvents = [...uiEvents];
if (isObservationEvent(event)) {
// Find and replace the corresponding action from uiEvents
const actionIndex = newUiEvents.findIndex(
(uiEvent) => uiEvent.id === event.action_id,
);
if (actionIndex !== -1) {
// Replace the action with the observation
newUiEvents[actionIndex] = event;
} else {
// Action not found in uiEvents, just add the observation
newUiEvents.push(event);
}
} else {
// For non-observation events, just add them to uiEvents
newUiEvents.push(event);
}
return newUiEvents;
};
+1
View File
@@ -25,6 +25,7 @@ export const MAP_PROVIDER = {
openrouter: "OpenRouter",
openhands: "OpenHands",
lemonade: "Lemonade",
clarifai: "Clarifai",
};
export const mapProvider = (provider: string) =>
+20
View File
@@ -0,0 +1,20 @@
/**
* Path manipulation utilities
*/
/**
* Strip workspace prefix from file paths
* Removes /workspace/ and the next directory level from paths
*
* @param path - The file path to process
* @returns The path with workspace prefix removed
*
* @example
* stripWorkspacePrefix("/workspace/repo/src/file.py") // returns "src/file.py"
* stripWorkspacePrefix("/workspace/my-project/components/Button.tsx") // returns "components/Button.tsx"
*/
export const stripWorkspacePrefix = (path: string): string => {
// Strip /workspace/ and the next directory level
const workspaceMatch = path.match(/^\/workspace\/[^/]+\/(.*)$/);
return workspaceMatch ? workspaceMatch[1] : path;
};
+8
View File
@@ -5,6 +5,7 @@ import { SuggestedTaskGroup } from "#/utils/types";
import { ConversationStatus } from "#/types/conversation-status";
import { GitRepository } from "#/types/git";
import { sanitizeQuery } from "#/utils/sanitize-query";
import { PRODUCT_URL } from "#/utils/constants";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
@@ -49,6 +50,13 @@ export const isMobileDevice = (): boolean =>
"ontouchstart" in window ||
navigator.maxTouchPoints > 0;
/**
* Checks if the current domain is the production domain
* @returns True if the current domain matches the production URL
*/
export const isProductionDomain = (): boolean =>
window.location.origin === PRODUCT_URL.PRODUCTION;
interface EventActionHistory {
args?: {
LLM_API_KEY?: string;
+1
View File
@@ -5,6 +5,7 @@ export const VERIFIED_PROVIDERS = [
"openai",
"mistral",
"lemonade",
"clarifai",
];
export const VERIFIED_MODELS = [
"o3-mini-2025-01-31",
+1 -1
View File
@@ -24,7 +24,7 @@ export function transformVSCodeUrl(vsCodeUrl: string | null): string | null {
}
return vsCodeUrl;
} catch (error) {
} catch {
// Silently handle the error and return the original URL
return vsCodeUrl;
}
+3
View File
@@ -30,6 +30,9 @@ vi.mock("#/hooks/use-is-on-tos-page", () => ({
useIsOnTosPage: () => false,
}));
// Import the Zustand mock to enable automatic store resets
vi.mock("zustand");
// Mock requests during tests
beforeAll(() => {
server.listen({ onUnhandledRequest: "bypass" });
+1
View File
@@ -27,6 +27,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a pull request, send the user a short message with a link to the pull request.
* Do NOT mark a pull request as ready to review unless the user explicitly says so
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch
+1
View File
@@ -28,6 +28,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a pull request, send the user a short message with a link to the pull request.
* Do NOT mark a pull request as ready to review unless the user explicitly says so
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch
+1
View File
@@ -27,6 +27,7 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
* Use the main branch as the base branch, unless the user requests otherwise
* After opening or updating a merge request, send the user a short message with a link to the merge request.
* IMPORTANT: When making commits, NEVER use the `--no-verify` flag. Pre-commit hooks (if configured in `.openhands/pre-commit.sh`) must be executed to ensure code quality and enforce project standards.
* Do all of the above in as few steps as possible. E.g. you could push changes with one step by running the following bash commands:
```bash
git remote -v && git branch # to find the current org, repo and branch
+1 -1
View File
@@ -164,7 +164,7 @@ def test_executable() -> bool:
)
# --- Wait for welcome ---
deadline = boot_start + 30
deadline = boot_start + 60
saw_welcome = False
captured = []
+7 -2
View File
@@ -1,3 +1,8 @@
"""OpenHands CLI package."""
"""OpenHands package."""
__version__ = '0.1.0'
from importlib.metadata import version, PackageNotFoundError
try:
__version__ = version("openhands")
except PackageNotFoundError:
__version__ = "0.0.0"
@@ -54,6 +54,7 @@ def _print_exit_hint(conversation_id: str) -> None:
)
def run_cli_entry(resume_conversation_id: str | None = None) -> None:
"""Run the agent chat session using the agent SDK.
+1 -10
View File
@@ -113,21 +113,12 @@ def launch_gui_server(mount_cwd: bool = False, gpu: bool = False) -> None:
pull_cmd = ['docker', 'pull', runtime_image]
print_formatted_text(HTML(_format_docker_command_for_logging(pull_cmd)))
try:
subprocess.run(
pull_cmd,
check=True,
timeout=300, # 5 minutes timeout
)
subprocess.run(pull_cmd, check=True)
except subprocess.CalledProcessError:
print_formatted_text(
HTML('<ansired>❌ Failed to pull runtime image.</ansired>')
)
sys.exit(1)
except subprocess.TimeoutExpired:
print_formatted_text(
HTML('<ansired>❌ Timeout while pulling runtime image.</ansired>')
)
sys.exit(1)
print_formatted_text('')
print_formatted_text(
-2
View File
@@ -57,8 +57,6 @@ def display_banner(conversation_id: str, resume: bool = False) -> None:
style=DEFAULT_STYLE,
)
print_formatted_text(HTML(f'<grey>OpenHands CLI v{__version__}</grey>'))
print_formatted_text('')
if not resume:
print_formatted_text(
@@ -1,3 +1,4 @@
import html
from prompt_toolkit import HTML, print_formatted_text
from openhands.sdk.security.confirmation_policy import (
@@ -37,7 +38,7 @@ def ask_user_confirmation(
or '[unknown action]'
)
print_formatted_text(
HTML(f'<grey> {i}. {tool_name}: {action_content}...</grey>')
HTML(f'<grey> {i}. {tool_name}: {html.escape(action_content)}...</grey>')
)
question = 'Choose an option:'
@@ -123,9 +123,15 @@ def prompt_api_key(
validator = NonEmptyValueValidator()
question = helper_text + step_counter.next_step(question)
return cli_text_input(
user_input = cli_text_input(
question, escapable=escapable, validator=validator, is_password=True
)
# If user pressed ENTER with existing key (empty input), return the existing key
if existing_api_key and not user_input.strip():
return existing_api_key.get_secret_value()
return user_input
# Advanced settings functions
+11 -7
View File
@@ -4,7 +4,7 @@ requires = [ "hatchling>=1.25" ]
[project]
name = "openhands"
version = "0.1.0"
version = "1.0.1"
description = "OpenHands CLI - Terminal User Interface for OpenHands AI Agent"
readme = "README.md"
license = { text = "MIT" }
@@ -15,15 +15,16 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
# Using Git URLs for dependencies so installs from PyPI pull from GitHub
# TODO: pin package versions once agent-sdk has published PyPI packages
dependencies = [
"openhands-sdk",
"openhands-tools",
"openhands-sdk @ git+https://github.com/All-Hands-AI/agent-sdk.git@50b094a92817e448ec4352d2950df4f19edd5a9f#subdirectory=openhands/sdk",
"openhands-tools @ git+https://github.com/All-Hands-AI/agent-sdk.git@50b094a92817e448ec4352d2950df4f19edd5a9f#subdirectory=openhands/tools",
"prompt-toolkit>=3",
"typer>=0.17.4",
]
# Dev-only tools with uv groups: `uv sync --group dev`
scripts.openhands = "openhands_cli.simple_main:main"
scripts = { openhands = "openhands_cli.simple_main:main" }
[dependency-groups]
# Hatchling wheel target: include the package directory
@@ -42,6 +43,9 @@ dev = [
"ruff>=0.11.8",
]
[tool.hatch.metadata]
allow-direct-references = true
[tool.hatch.build.targets.wheel]
packages = [ "openhands_cli" ]
@@ -96,5 +100,5 @@ disallow_untyped_defs = true
ignore_missing_imports = true
[tool.uv.sources]
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "189979a5013751aa86852ab41afe9a79555e62ac" }
openhands-sdk = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/sdk", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
openhands-tools = { git = "https://github.com/All-Hands-AI/agent-sdk.git", subdirectory = "openhands/tools", rev = "50b094a92817e448ec4352d2950df4f19edd5a9f" }
@@ -0,0 +1,56 @@
"""Test for API key preservation bug when updating settings."""
from unittest.mock import patch
import pytest
from pydantic import SecretStr
from openhands_cli.user_actions.settings_action import prompt_api_key
from openhands_cli.tui.utils import StepCounter
def test_api_key_preservation_when_user_presses_enter():
"""Test that API key is preserved when user presses ENTER to keep current key.
This test replicates the bug where API keys disappear when updating settings.
When a user presses ENTER to keep the current API key, the function should
return the existing API key, not an empty string.
"""
step_counter = StepCounter(1)
existing_api_key = SecretStr("sk-existing-key-123")
# Mock cli_text_input to return empty string (simulating user pressing ENTER)
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=''):
result = prompt_api_key(
step_counter=step_counter,
provider='openai',
existing_api_key=existing_api_key,
escapable=True
)
# The bug: result is empty string instead of the existing key
# This test will fail initially, demonstrating the bug
assert result == existing_api_key.get_secret_value(), (
f"Expected existing API key '{existing_api_key.get_secret_value()}' "
f"but got '{result}'. API key should be preserved when user presses ENTER."
)
def test_api_key_update_when_user_enters_new_key():
"""Test that API key is updated when user enters a new key."""
step_counter = StepCounter(1)
existing_api_key = SecretStr("sk-existing-key-123")
new_api_key = "sk-new-key-456"
# Mock cli_text_input to return new API key
with patch('openhands_cli.user_actions.settings_action.cli_text_input', return_value=new_api_key):
result = prompt_api_key(
step_counter=step_counter,
provider='openai',
existing_api_key=existing_api_key,
escapable=True
)
# Should return the new API key
assert result == new_api_key
-2
View File
@@ -111,8 +111,6 @@ class TestLaunchGuiServer:
[
# Docker pull failure
(subprocess.CalledProcessError(1, 'docker pull'), None, 1, False, False),
# Docker pull timeout
(subprocess.TimeoutExpired('docker pull', 300), None, 1, False, False),
# Docker run failure
(MagicMock(returncode=0), subprocess.CalledProcessError(1, 'docker run'), 1, False, False),
# KeyboardInterrupt during run
+32 -21
View File
@@ -660,18 +660,32 @@ wheels = [
[[package]]
name = "fastuuid"
version = "0.12.0"
version = "0.13.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/17/13146a1e916bd2971d0a58db5e0a4ad23efdd49f78f33ac871c161f8007b/fastuuid-0.12.0.tar.gz", hash = "sha256:d0bd4e5b35aad2826403f4411937c89e7c88857b1513fe10f696544c03e9bd8e", size = 19180, upload-time = "2025-01-27T18:04:14.387Z" }
sdist = { url = "https://files.pythonhosted.org/packages/15/80/3c16a1edad2e6cd82fbd15ac998cc1b881f478bf1f80ca717d941c441874/fastuuid-0.13.5.tar.gz", hash = "sha256:d4976821ab424d41542e1ea39bc828a9d454c3f8a04067c06fca123c5b95a1a1", size = 18255, upload-time = "2025-09-26T09:05:38.281Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f6/28/442e79d6219b90208cb243ac01db05d89cc4fdf8ecd563fb89476baf7122/fastuuid-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:328694a573fe9dce556b0b70c9d03776786801e028d82f0b6d9db1cb0521b4d1", size = 247372, upload-time = "2025-01-27T18:03:40.967Z" },
{ url = "https://files.pythonhosted.org/packages/40/eb/e0fd56890970ca7a9ec0d116844580988b692b1a749ac38e0c39e1dbdf23/fastuuid-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02acaea2c955bb2035a7d8e7b3fba8bd623b03746ae278e5fa932ef54c702f9f", size = 258200, upload-time = "2025-01-27T18:04:12.138Z" },
{ url = "https://files.pythonhosted.org/packages/f5/3c/4b30e376e65597a51a3dc929461a0dec77c8aec5d41d930f482b8f43e781/fastuuid-0.12.0-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:ed9f449cba8cf16cced252521aee06e633d50ec48c807683f21cc1d89e193eb0", size = 278446, upload-time = "2025-01-27T18:04:15.877Z" },
{ url = "https://files.pythonhosted.org/packages/fe/96/cc5975fd23d2197b3e29f650a7a9beddce8993eaf934fa4ac595b77bb71f/fastuuid-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:0df2ea4c9db96fd8f4fa38d0e88e309b3e56f8fd03675a2f6958a5b082a0c1e4", size = 157185, upload-time = "2025-01-27T18:06:19.21Z" },
{ url = "https://files.pythonhosted.org/packages/a9/e8/d2bb4f19e5ee15f6f8e3192a54a897678314151aa17d0fb766d2c2cbc03d/fastuuid-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7fe2407316a04ee8f06d3dbc7eae396d0a86591d92bafe2ca32fce23b1145786", size = 247512, upload-time = "2025-01-27T18:04:08.115Z" },
{ url = "https://files.pythonhosted.org/packages/bc/53/25e811d92fd60f5c65e098c3b68bd8f1a35e4abb6b77a153025115b680de/fastuuid-0.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9b31dd488d0778c36f8279b306dc92a42f16904cba54acca71e107d65b60b0c", size = 258257, upload-time = "2025-01-27T18:03:56.408Z" },
{ url = "https://files.pythonhosted.org/packages/10/23/73618e7793ea0b619caae2accd9e93e60da38dd78dd425002d319152ef2f/fastuuid-0.12.0-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:b19361ee649365eefc717ec08005972d3d1eb9ee39908022d98e3bfa9da59e37", size = 278559, upload-time = "2025-01-27T18:03:58.661Z" },
{ url = "https://files.pythonhosted.org/packages/e4/41/6317ecfc4757d5f2a604e5d3993f353ba7aee85fa75ad8b86fce6fc2fa40/fastuuid-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:8fc66b11423e6f3e1937385f655bedd67aebe56a3dcec0cb835351cfe7d358c9", size = 157276, upload-time = "2025-01-27T18:06:39.245Z" },
{ url = "https://files.pythonhosted.org/packages/21/36/434f137c5970cac19e57834e1f7680e85301619d49891618c00666700c61/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:35fe8045e866bc6846f8de6fa05acb1de0c32478048484a995e96d31e21dff2a", size = 494638, upload-time = "2025-09-26T09:14:58.695Z" },
{ url = "https://files.pythonhosted.org/packages/ca/3c/083de2ac007b2b305523b9c006dba5051e5afd87a626ef1a39f76e2c6b82/fastuuid-0.13.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:02a460333f52d731a006d18a52ef6fcb2d295a1f5b1a5938d30744191b2f77b7", size = 253138, upload-time = "2025-09-26T09:13:33.283Z" },
{ url = "https://files.pythonhosted.org/packages/73/5e/630cffa1c8775db526e39e9e4c5c7db0c27be0786bb21ba82c912ae19f63/fastuuid-0.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:74b0e4f8c307b9f477a5d7284db4431ce53a3c1e3f4173db7a97db18564a6202", size = 244521, upload-time = "2025-09-26T09:14:40.682Z" },
{ url = "https://files.pythonhosted.org/packages/4d/51/55d78705f4fbdadf88fb40f382f508d6c7a4941ceddd7825fafebb4cc778/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6955a99ef455c2986f3851f4e0ccc35dec56ac1a7720f2b92e88a75d6684512e", size = 271557, upload-time = "2025-09-26T09:15:09.75Z" },
{ url = "https://files.pythonhosted.org/packages/6a/2b/1b89e90a8635e5587ccdbbeb169c590672ce7637880f2c047482a0359950/fastuuid-0.13.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f10c77b826738c1a27dcdaa92ea4dc1ec9d869748a99e1fde54f1379553d4854", size = 272334, upload-time = "2025-09-26T09:07:48.865Z" },
{ url = "https://files.pythonhosted.org/packages/0c/06/4c8207894eeb30414999e5c3f66ac039bc4003437eb4060d8a1bceb4cc6f/fastuuid-0.13.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bb25dccbeb249d16d5e664f65f17ebec05136821d5ef462c4110e3f76b86fb86", size = 290594, upload-time = "2025-09-26T09:12:54.124Z" },
{ url = "https://files.pythonhosted.org/packages/50/69/96d221931a31d77a47cc2487bdfacfb3091edfc2e7a04b1795df1aec05df/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a5becc646a3eeafb76ce0a6783ba190cd182e3790a8b2c78ca9db2b5e87af952", size = 452835, upload-time = "2025-09-26T09:14:00.994Z" },
{ url = "https://files.pythonhosted.org/packages/25/ef/bf045f0a47dcec96247497ef3f7a31d86ebc074330e2dccc34b8dbc0468a/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69b34363752d06e9bb0dbdf02ae391ec56ac948c6f2eb00be90dad68e80774b9", size = 468225, upload-time = "2025-09-26T09:13:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/30/46/4817ab5a3778927155a4bde92540d4c4fa996161ec8b8e080c8928b0984e/fastuuid-0.13.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:57d0768afcad0eab8770c9b8cf904716bd3c547e8b9a4e755ee8a673b060a3a3", size = 444907, upload-time = "2025-09-26T09:14:30.163Z" },
{ url = "https://files.pythonhosted.org/packages/80/27/ab284117ce4dc9b356a7196bdbf220510285f201d27f1f078592cdc8187b/fastuuid-0.13.5-cp312-cp312-win32.whl", hash = "sha256:8ac6c6f5129d52eaa6ef9ea4b6e2f7c69468a053f3ab8e439661186b9c06bb85", size = 145415, upload-time = "2025-09-26T09:08:59.494Z" },
{ url = "https://files.pythonhosted.org/packages/f4/0c/f970a4222773b248931819f8940800b760283216ca3dda173ed027e94bdd/fastuuid-0.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:ad630e97715beefef07ec37c9c162336e500400774e2c1cbe1a0df6f80d15b9a", size = 150840, upload-time = "2025-09-26T09:13:46.115Z" },
{ url = "https://files.pythonhosted.org/packages/4f/62/74fc53f6e04a4dc5b36c34e4e679f85a4c14eec800dcdb0f2c14b5442217/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:ea17dfd35e0e91920a35d91e65e5f9c9d1985db55ac4ff2f1667a0f61189cefa", size = 494678, upload-time = "2025-09-26T09:14:30.908Z" },
{ url = "https://files.pythonhosted.org/packages/09/ba/f28b9b7045738a8bfccfb9cd6aff4b91fce2669e6b383a48b0694ee9b3ff/fastuuid-0.13.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:be6ad91e5fefbcc2a4b478858a2715e386d405834ea3ae337c3b6b95cc0e47d6", size = 253162, upload-time = "2025-09-26T09:13:35.879Z" },
{ url = "https://files.pythonhosted.org/packages/b1/18/13fac89cb4c9f0cd7e81a9154a77ecebcc95d2b03477aa91d4d50f7227ee/fastuuid-0.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ea6df13a306aab3e0439d58c312ff1e6f4f07f09f667579679239b4a6121f64a", size = 244546, upload-time = "2025-09-26T09:14:58.13Z" },
{ url = "https://files.pythonhosted.org/packages/04/bf/9691167804d59411cc4269841df949f6dd5e76452ab10dcfcd1dbe04c5bc/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2354c1996d3cf12dc2ba3752e2c4d6edc46e1a38c63893146777b1939f3062d4", size = 271528, upload-time = "2025-09-26T09:14:48.996Z" },
{ url = "https://files.pythonhosted.org/packages/a9/b5/7a75a03d1c7aa0b6d573032fcca39391f0aef7f2caabeeb45a672bc0bd3c/fastuuid-0.13.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6cf9b7469fc26d1f9b1c43ac4b192e219e85b88fdf81d71aa755a6c08c8a817", size = 272292, upload-time = "2025-09-26T09:14:42.82Z" },
{ url = "https://files.pythonhosted.org/packages/c0/db/fa0f16cbf76e6880599533af4ef01bb586949c5320612e9d884eff13e603/fastuuid-0.13.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:92ba539170097b9047551375f1ca09d8d2b4aefcc79eeae3e1c43fe49b42072e", size = 290466, upload-time = "2025-09-26T09:08:33.161Z" },
{ url = "https://files.pythonhosted.org/packages/1e/02/6b8c45bfbc8500994dd94edba7f59555f9683c4d8c9a164ae1d25d03c7c7/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:dbb81d05617bc2970765c1ad82db7e8716f6a2b7a361a14b83de5b9240ade448", size = 452838, upload-time = "2025-09-26T09:13:44.747Z" },
{ url = "https://files.pythonhosted.org/packages/27/12/85d95a84f265b888e8eb9f9e2b5aaf331e8be60c0a7060146364b3544b6a/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:d973bd6bf9d754d3cca874714ac0a6b22a47f239fb3d3c8687569db05aac3471", size = 468149, upload-time = "2025-09-26T09:13:18.712Z" },
{ url = "https://files.pythonhosted.org/packages/ad/da/dd9a137e9ea707e883c92470113a432233482ec9ad3e9b99c4defc4904e6/fastuuid-0.13.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e725ceef79486423f05ee657634d4b4c1ca5fb2c8a94e0708f5d6356a83f2a83", size = 444933, upload-time = "2025-09-26T09:14:09.494Z" },
{ url = "https://files.pythonhosted.org/packages/12/f4/ab363d7f4ac3989691e2dc5ae2d8391cfb0b4169e52ef7fa0ac363e936f0/fastuuid-0.13.5-cp313-cp313-win32.whl", hash = "sha256:a1c430a332ead0b2674f1ef71b17f43b8139ec5a4201182766a21f131a31e021", size = 145462, upload-time = "2025-09-26T09:14:15.105Z" },
{ url = "https://files.pythonhosted.org/packages/aa/8a/52eb77d9c294a54caa0d2d8cc9f906207aa6d916a22de963687ab6db8b86/fastuuid-0.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:241fdd362fd96e6b337db62a65dd7cb3dfac20adf854573247a47510e192db6f", size = 150923, upload-time = "2025-09-26T09:13:03.923Z" },
]
[[package]]
@@ -1266,8 +1280,8 @@ wheels = [
[[package]]
name = "litellm"
version = "1.76.2"
source = { registry = "https://pypi.org/simple" }
version = "1.77.7"
source = { git = "https://github.com/BerriAI/litellm.git?rev=v1.77.7.dev9#763d2f8ccdd8412dbe6d4ac0e136d9ac34dcd4c0" }
dependencies = [
{ name = "aiohttp" },
{ name = "click" },
@@ -1282,10 +1296,6 @@ dependencies = [
{ name = "tiktoken" },
{ name = "tokenizers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/75/a3/f7c00c660972eed1ba5ed53771ac9b4235e7fb1dc410e91d35aff2778ae7/litellm-1.76.2.tar.gz", hash = "sha256:fc7af111fa0f06943d8dbebed73f88000f9902f0d0ee0882c57d0bd5c1a37ecb", size = 10189238, upload-time = "2025-09-04T00:25:09.472Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/f4/980cc81c21424026dcb48a541654fd6f4286891825a3d0dd51f02b65cbc3/litellm-1.76.2-py3-none-any.whl", hash = "sha256:a9a2ef64a598b5b4ae245f1de6afc400856477cd6f708ff633d95e2275605a45", size = 8973847, upload-time = "2025-09-04T00:25:05.353Z" },
]
[[package]]
name = "macholib"
@@ -1615,7 +1625,7 @@ wheels = [
[[package]]
name = "openhands"
version = "0.1.0"
version = "1.0.1"
source = { editable = "." }
dependencies = [
{ name = "openhands-sdk" },
@@ -1642,8 +1652,8 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=189979a5013751aa86852ab41afe9a79555e62ac" },
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=189979a5013751aa86852ab41afe9a79555e62ac" },
{ name = "openhands-sdk", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=50b094a92817e448ec4352d2950df4f19edd5a9f" },
{ name = "openhands-tools", git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=50b094a92817e448ec4352d2950df4f19edd5a9f" },
{ name = "prompt-toolkit", specifier = ">=3" },
{ name = "typer", specifier = ">=0.17.4" },
]
@@ -1667,9 +1677,10 @@ dev = [
[[package]]
name = "openhands-sdk"
version = "1.0.0"
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=189979a5013751aa86852ab41afe9a79555e62ac#189979a5013751aa86852ab41afe9a79555e62ac" }
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Fsdk&rev=50b094a92817e448ec4352d2950df4f19edd5a9f#50b094a92817e448ec4352d2950df4f19edd5a9f" }
dependencies = [
{ name = "fastmcp" },
{ name = "httpx" },
{ name = "litellm" },
{ name = "pydantic" },
{ name = "python-frontmatter" },
@@ -1681,7 +1692,7 @@ dependencies = [
[[package]]
name = "openhands-tools"
version = "1.0.0"
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=189979a5013751aa86852ab41afe9a79555e62ac#189979a5013751aa86852ab41afe9a79555e62ac" }
source = { git = "https://github.com/All-Hands-AI/agent-sdk.git?subdirectory=openhands%2Ftools&rev=50b094a92817e448ec4352d2950df4f19edd5a9f#50b094a92817e448ec4352d2950df4f19edd5a9f" }
dependencies = [
{ name = "bashlex" },
{ name = "binaryornot" },
+7 -42
View File
@@ -1,44 +1,9 @@
import os
from pathlib import Path
# This is a namespace package - extend the path to include installed packages
# (We need to do this to support dependencies openhands-sdk, openhands-tools and openhands-agent-server
# which all have a top level `openhands`` package.)
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__package_name__ = 'openhands_ai'
# Import version information for backward compatibility
from openhands.version import __version__, get_version
def get_version():
# Try getting the version from pyproject.toml
try:
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
candidate_paths = [
Path(root_dir) / 'pyproject.toml',
Path(root_dir) / 'openhands' / 'pyproject.toml',
]
for file_path in candidate_paths:
if file_path.is_file():
with open(file_path, 'r') as f:
for line in f:
if line.strip().startswith('version ='):
return line.split('=', 1)[1].strip().strip('"').strip("'")
except FileNotFoundError:
pass
try:
from importlib.metadata import PackageNotFoundError, version
return version(__package_name__)
except (ImportError, PackageNotFoundError):
pass
try:
from pkg_resources import DistributionNotFound, get_distribution # type: ignore
return get_distribution(__package_name__).version
except (ImportError, DistributionNotFound):
pass
return 'unknown'
try:
__version__ = get_version()
except Exception:
__version__ = 'unknown'
__all__ = ['__version__', 'get_version']
@@ -3,9 +3,9 @@
At the user's request, repository {{ repository_info.repo_name }} has been cloned to {{ repository_info.repo_directory }} in the current working directory.
{% if repository_info.branch_name %}The repository has been checked out to branch "{{ repository_info.branch_name }}".
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless
IMPORTANT: You should work within the current branch "{{ repository_info.branch_name }}" unless:
1. the user explicitly instructs otherwise
2. if the current branch is "main", "master", or another default branch where direct pushes may be unsafe
2. the current branch is "main", "master", or another default branch where direct pushes may be unsafe
{% endif %}
</REPOSITORY_INFO>
{% endif %}
@@ -35,9 +35,9 @@ For example, if you are using vite.config.js, you should set server.host and ser
{% endif %}
{% if runtime_info.custom_secrets_descriptions %}
<CUSTOM_SECRETS>
You are have access to the following environment variables
You have access to the following environment variables
{% for secret_name, secret_description in runtime_info.custom_secrets_descriptions.items() %}
* $**{{ secret_name }}**: {{ secret_description }}
* **${{ secret_name }}**: {{ secret_description }}
{% endfor %}
</CUSTOM_SECRETS>
{% endif %}
@@ -35,6 +35,7 @@ Your primary role is to assist users by executing commands, modifying code, and
* If there are existing git user credentials already configured, use them and add Co-authored-by: openhands <openhands@all-hands.dev> to any commits messages you make. if a git config doesn't exist use "openhands" as the user.name and "openhands@all-hands.dev" as the user.email by default, unless explicitly instructed otherwise.
* Exercise caution with git operations. Do NOT make potentially dangerous changes (e.g., pushing to main, deleting repositories) unless explicitly asked to do so.
* When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
* NEVER use the `--no-verify` flag when making commits. Pre-commit hooks must be allowed to run to enforce code quality standards and project-specific checks configured in `.openhands/pre-commit.sh`.
* Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
* If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
</VERSION_CONTROL>

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