mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75a4033bef | |||
| 2f9a4f96ae | |||
| ac152da39e | |||
| 4a4f213f57 | |||
| 77210bc678 | |||
| 425b4f77a9 | |||
| f9099fe6db | |||
| 8f46a0a7a3 | |||
| 55d204ae1b | |||
| 4d7cd228da | |||
| a3f92df4b3 | |||
| e41f8f5215 | |||
| 6448f5a681 | |||
| a84670e6bf | |||
| fc3b3f733f | |||
| 150f56f252 | |||
| 05171f08fb | |||
| 64a160cf03 | |||
| 8b1f38e52e | |||
| 868f434f97 | |||
| 210f7fc653 | |||
| f9512cd234 | |||
| ff8e659905 | |||
| c32610a440 | |||
| 26abb81a86 | |||
| 23121301eb | |||
| 97cd7eb0a2 | |||
| 12e8183336 | |||
| e075873962 | |||
| ae1288a3e8 | |||
| 3a2327a879 | |||
| d1f40ffab1 | |||
| 31241f2490 | |||
| 08ffd53636 | |||
| c88173c1df | |||
| 5f8f4fd42c | |||
| 310fa75535 | |||
| a0dfbe39bf | |||
| b69b01cc15 | |||
| acf8539a3f | |||
| 08c6686026 | |||
| 691f42cf77 | |||
| 39049b173a | |||
| 1b911ea5ba | |||
| 971fcd7296 | |||
| a43778bf24 | |||
| 67278772ad | |||
| 33ec22ff91 | |||
| c8dd141997 | |||
| e2a56c9302 | |||
| 1d53587cbd | |||
| 880a798152 | |||
| a356b09f30 | |||
| 01e91cebe4 | |||
| 93cbba4e06 | |||
| df45db5fa1 | |||
| 36c628014f | |||
| ff38146cb6 | |||
| 7512ffc16a | |||
| aa545c29f1 | |||
| 54cdc5c744 | |||
| a5afc1ff7a | |||
| ecf23d3e74 | |||
| c2e080a340 | |||
| 4b2ca6ca71 | |||
| 42d1a54670 | |||
| 25261673a1 | |||
| d025d225ad |
+13
-13
@@ -58,34 +58,34 @@ RUN sed -i 's/^UID_MIN.*/UID_MIN 499/' /etc/login.defs
|
||||
# Default is 60000, but we've seen up to 200000
|
||||
RUN sed -i 's/^UID_MAX.*/UID_MAX 1000000/' /etc/login.defs
|
||||
|
||||
RUN groupadd --gid $OPENHANDS_USER_ID app
|
||||
RUN groupadd --gid $OPENHANDS_USER_ID openhands
|
||||
RUN useradd -l -m -u $OPENHANDS_USER_ID --gid $OPENHANDS_USER_ID -s /bin/bash openhands && \
|
||||
usermod -aG app openhands && \
|
||||
usermod -aG openhands openhands && \
|
||||
usermod -aG sudo openhands && \
|
||||
echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
|
||||
RUN chown -R openhands:app /app && chmod -R 770 /app
|
||||
RUN sudo chown -R openhands:app $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
|
||||
RUN chown -R openhands:openhands /app && chmod -R 770 /app
|
||||
RUN sudo chown -R openhands:openhands $WORKSPACE_BASE && sudo chmod -R 770 $WORKSPACE_BASE
|
||||
USER openhands
|
||||
|
||||
ENV VIRTUAL_ENV=/app/.venv \
|
||||
PATH="/app/.venv/bin:$PATH" \
|
||||
PYTHONPATH='/app'
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
COPY --chown=openhands:openhands --chmod=770 --from=backend-builder ${VIRTUAL_ENV} ${VIRTUAL_ENV}
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 ./microagents ./microagents
|
||||
COPY --chown=openhands:app --chmod=770 ./openhands ./openhands
|
||||
COPY --chown=openhands:app --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
|
||||
COPY --chown=openhands:app pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
|
||||
COPY --chown=openhands:openhands --chmod=770 ./microagents ./microagents
|
||||
COPY --chown=openhands:openhands --chmod=770 ./openhands ./openhands
|
||||
COPY --chown=openhands:openhands --chmod=777 ./openhands/runtime/plugins ./openhands/runtime/plugins
|
||||
COPY --chown=openhands:openhands pyproject.toml poetry.lock README.md MANIFEST.in LICENSE ./
|
||||
|
||||
# This is run as "openhands" user, and will create __pycache__ with openhands:openhands ownership
|
||||
RUN python openhands/core/download.py # No-op to download assets
|
||||
# Add this line to set group ownership of all files/directories not already in "app" group
|
||||
# openhands:openhands -> openhands:app
|
||||
RUN find /app \! -group app -exec chgrp app {} +
|
||||
# openhands:openhands -> openhands:openhands
|
||||
RUN find /app \! -group openhands -exec chgrp openhands {} +
|
||||
|
||||
COPY --chown=openhands:app --chmod=770 --from=frontend-builder /app/build ./frontend/build
|
||||
COPY --chown=openhands:app --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
|
||||
COPY --chown=openhands:openhands --chmod=770 --from=frontend-builder /app/build ./frontend/build
|
||||
COPY --chown=openhands:openhands --chmod=770 ./containers/app/entrypoint.sh /app/entrypoint.sh
|
||||
|
||||
USER root
|
||||
|
||||
|
||||
@@ -45,6 +45,13 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
1. [Install WSL](https://learn.microsoft.com/en-us/windows/wsl/install).
|
||||
2. Run `wsl --version` in powershell and confirm `Default Version: 2`.
|
||||
|
||||
**Ubuntu (Linux Distribution)**
|
||||
|
||||
1. Install Ubuntu: `wsl --install -d Ubuntu` in PowerShell as Administrator.
|
||||
2. Restart computer when prompted.
|
||||
3. Open Ubuntu from Start menu to complete setup.
|
||||
4. Verify installation: `wsl --list` should show Ubuntu.
|
||||
|
||||
**Docker Desktop**
|
||||
|
||||
1. [Install Docker Desktop on Windows](https://docs.docker.com/desktop/setup/install/windows-install).
|
||||
@@ -53,7 +60,7 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
|
||||
- Resources > WSL Integration: `Enable integration with my default WSL distro` is enabled.
|
||||
|
||||
<Note>
|
||||
The docker command below to start the app must be run inside the WSL terminal.
|
||||
The docker command below to start the app must be run inside the WSL terminal. Use `wsl -d Ubuntu` in PowerShell or search "Ubuntu" in the Start menu to access the Ubuntu terminal.
|
||||
</Note>
|
||||
|
||||
**Alternative: Windows without WSL**
|
||||
|
||||
Generated
+164
-271
File diff suppressed because it is too large
Load Diff
+21
-21
@@ -11,17 +11,17 @@
|
||||
"@heroui/use-infinite-scroll": "^2.2.10",
|
||||
"@microlink/react-json-view": "^1.26.2",
|
||||
"@monaco-editor/react": "^4.7.0-rc.0",
|
||||
"@react-router/node": "^7.8.0",
|
||||
"@react-router/serve": "^7.8.0",
|
||||
"@react-router/node": "^7.8.2",
|
||||
"@react-router/serve": "^7.8.2",
|
||||
"@react-types/shared": "^3.31.0",
|
||||
"@reduxjs/toolkit": "^2.8.2",
|
||||
"@stripe/react-stripe-js": "^3.9.0",
|
||||
"@stripe/react-stripe-js": "^3.9.1",
|
||||
"@stripe/stripe-js": "^7.8.0",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@tailwindcss/vite": "^4.1.12",
|
||||
"@tanstack/react-query": "^5.85.3",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"@vitejs/plugin-react": "^5.0.1",
|
||||
"@xterm/addon-fit": "^0.10.0",
|
||||
"@xterm/xterm": "^5.4.0",
|
||||
"axios": "^1.11.0",
|
||||
@@ -29,32 +29,32 @@
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||
"framer-motion": "^12.23.12",
|
||||
"i18next": "^25.3.6",
|
||||
"i18next": "^25.4.2",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"isbot": "^5.1.29",
|
||||
"jose": "^6.0.12",
|
||||
"lucide-react": "^0.539.0",
|
||||
"isbot": "^5.1.30",
|
||||
"jose": "^6.0.13",
|
||||
"lucide-react": "^0.541.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"posthog-js": "^1.260.1",
|
||||
"posthog-js": "^1.260.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-highlight": "^0.15.0",
|
||||
"react-hot-toast": "^2.5.1",
|
||||
"react-i18next": "^15.6.1",
|
||||
"react-hot-toast": "^2.6.0",
|
||||
"react-i18next": "^15.7.2",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-router": "^7.8.0",
|
||||
"react-router": "^7.8.2",
|
||||
"react-select": "^5.10.2",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"react-syntax-highlighter": "^15.6.5",
|
||||
"react-textarea-autosize": "^8.5.9",
|
||||
"remark-breaks": "^4.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"sirv-cli": "^3.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"vite": "^7.1.1",
|
||||
"vite": "^7.1.3",
|
||||
"web-vitals": "^5.1.0",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
@@ -88,16 +88,16 @@
|
||||
"@babel/traverse": "^7.28.3",
|
||||
"@babel/types": "^7.28.2",
|
||||
"@mswjs/socket.io-binding": "^0.2.0",
|
||||
"@playwright/test": "^1.54.2",
|
||||
"@react-router/dev": "^7.8.0",
|
||||
"@playwright/test": "^1.55.0",
|
||||
"@react-router/dev": "^7.8.2",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tanstack/eslint-plugin-query": "^5.83.1",
|
||||
"@testing-library/dom": "^10.4.1",
|
||||
"@testing-library/jest-dom": "^6.7.0",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^24.2.0",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.11",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@types/react-highlight": "^0.12.8",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
@@ -126,7 +126,7 @@
|
||||
"stripe": "^18.4.0",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vite-plugin-svgr": "^4.5.0",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.0.2"
|
||||
},
|
||||
|
||||
@@ -23,9 +23,9 @@ export function ConfirmStopModal({
|
||||
<ModalBackdrop>
|
||||
<ModalBody className="items-start border border-tertiary">
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_STOP)} />
|
||||
<BaseModalTitle title={t(I18nKey.CONVERSATION$CONFIRM_PAUSE)} />
|
||||
<BaseModalDescription
|
||||
description={t(I18nKey.CONVERSATION$STOP_WARNING)}
|
||||
description={t(I18nKey.CONVERSATION$PAUSE_WARNING)}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
|
||||
+1
-1
@@ -129,7 +129,7 @@ export function ConversationCardContextMenu({
|
||||
|
||||
{onStop && (
|
||||
<ContextMenuListItem testId="stop-button" onClick={onStop}>
|
||||
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$STOP)} />
|
||||
<ContextMenuIconText icon={Power} text={t(I18nKey.BUTTON$PAUSE)} />
|
||||
</ContextMenuListItem>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { ConversationStatus } from "#/types/conversation-status";
|
||||
import ArchivedIcon from "./state-indicators/archived.svg?react";
|
||||
import ErrorIcon from "./state-indicators/error.svg?react";
|
||||
import RunningIcon from "./state-indicators/running.svg?react";
|
||||
import StartingIcon from "./state-indicators/starting.svg?react";
|
||||
import StoppedIcon from "./state-indicators/stopped.svg?react";
|
||||
@@ -9,6 +11,8 @@ const CONVERSATION_STATUS_INDICATORS: Record<ConversationStatus, SVGIcon> = {
|
||||
STOPPED: StoppedIcon,
|
||||
RUNNING: RunningIcon,
|
||||
STARTING: StartingIcon,
|
||||
ARCHIVED: ArchivedIcon,
|
||||
ERROR: ErrorIcon,
|
||||
};
|
||||
|
||||
interface ConversationStateIndicatorProps {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#A7A9AC"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M17 7h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1 0 1.43-.98 2.63-2.31 2.98l1.46 1.46C20.88 15.61 22 13.95 22 12c0-2.76-2.24-5-5-5zm-1 4h-2.19l2 2H16zM2 4.27l3.11 3.11C3.29 8.12 2 9.91 2 12c0 2.76 2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1 0-1.59 1.21-2.9 2.76-3.07L8.73 11H8v2h2.73L13 15.27V17h1.73l4.01 4L20 19.74 3.27 3 2 4.27z"/><path d="M0 24V0" fill="none"/></svg>
|
||||
|
After Width: | Height: | Size: 512 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#e7000b"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/></svg>
|
||||
|
After Width: | Height: | Size: 254 B |
@@ -19,6 +19,8 @@ const saveSettingsMutationFn = async (settings: Partial<PostSettings>) => {
|
||||
: settings.llm_api_key?.trim() || undefined,
|
||||
remote_runtime_resource_factor: settings.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
enable_default_condenser: settings.ENABLE_DEFAULT_CONDENSER,
|
||||
condenser_max_size:
|
||||
settings.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
|
||||
enable_sound_notifications: settings.ENABLE_SOUND_NOTIFICATIONS,
|
||||
user_consents_to_analytics: settings.user_consents_to_analytics,
|
||||
provider_tokens_set: settings.PROVIDER_TOKENS_SET,
|
||||
|
||||
@@ -22,6 +22,8 @@ const getSettingsQueryFn = async (): Promise<Settings> => {
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: apiSettings.remote_runtime_resource_factor,
|
||||
PROVIDER_TOKENS_SET: apiSettings.provider_tokens_set,
|
||||
ENABLE_DEFAULT_CONDENSER: apiSettings.enable_default_condenser,
|
||||
CONDENSER_MAX_SIZE:
|
||||
apiSettings.condenser_max_size ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
|
||||
ENABLE_SOUND_NOTIFICATIONS: apiSettings.enable_sound_notifications,
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS:
|
||||
apiSettings.enable_proactive_conversation_starters,
|
||||
|
||||
@@ -97,6 +97,8 @@ export enum I18nKey {
|
||||
SETTINGS$BASE_URL = "SETTINGS$BASE_URL",
|
||||
SETTINGS$AGENT = "SETTINGS$AGENT",
|
||||
SETTINGS$ENABLE_MEMORY_CONDENSATION = "SETTINGS$ENABLE_MEMORY_CONDENSATION",
|
||||
SETTINGS$CONDENSER_MAX_SIZE = "SETTINGS$CONDENSER_MAX_SIZE",
|
||||
SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP = "SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP",
|
||||
SETTINGS$LANGUAGE = "SETTINGS$LANGUAGE",
|
||||
ACTION$PUSH_TO_BRANCH = "ACTION$PUSH_TO_BRANCH",
|
||||
ACTION$PUSH_CREATE_PR = "ACTION$PUSH_CREATE_PR",
|
||||
@@ -326,6 +328,7 @@ export enum I18nKey {
|
||||
USER$ACCOUNT_SETTINGS = "USER$ACCOUNT_SETTINGS",
|
||||
JUPYTER$OUTPUT_LABEL = "JUPYTER$OUTPUT_LABEL",
|
||||
BUTTON$STOP = "BUTTON$STOP",
|
||||
BUTTON$PAUSE = "BUTTON$PAUSE",
|
||||
BUTTON$EDIT_TITLE = "BUTTON$EDIT_TITLE",
|
||||
BUTTON$DOWNLOAD_VIA_VSCODE = "BUTTON$DOWNLOAD_VIA_VSCODE",
|
||||
BUTTON$DISPLAY_COST = "BUTTON$DISPLAY_COST",
|
||||
@@ -337,6 +340,8 @@ export enum I18nKey {
|
||||
LANDING$RECENT_CONVERSATION = "LANDING$RECENT_CONVERSATION",
|
||||
CONVERSATION$CONFIRM_DELETE = "CONVERSATION$CONFIRM_DELETE",
|
||||
CONVERSATION$CONFIRM_STOP = "CONVERSATION$CONFIRM_STOP",
|
||||
CONVERSATION$CONFIRM_PAUSE = "CONVERSATION$CONFIRM_PAUSE",
|
||||
CONVERSATION$PAUSE_WARNING = "CONVERSATION$PAUSE_WARNING",
|
||||
CONVERSATION$STOP_WARNING = "CONVERSATION$STOP_WARNING",
|
||||
CONVERSATION$METRICS_INFO = "CONVERSATION$METRICS_INFO",
|
||||
CONVERSATION$CREATED = "CONVERSATION$CREATED",
|
||||
|
||||
@@ -1551,6 +1551,38 @@
|
||||
"de": "Speicherkondensation aktivieren",
|
||||
"uk": "Увімкнути конденсацію пам'яті"
|
||||
},
|
||||
"SETTINGS$CONDENSER_MAX_SIZE": {
|
||||
"en": "Memory condenser max history size",
|
||||
"ja": "メモリ凝縮の最大履歴サイズ",
|
||||
"zh-CN": "内存凝缩最大历史大小",
|
||||
"zh-TW": "記憶體凝縮最大歷史大小",
|
||||
"ko-KR": "메모리 응축 최대 기록 크기",
|
||||
"no": "Maks historikkstørrelse for minnekondenser",
|
||||
"it": "Dimensione massima cronologia condensatore di memoria",
|
||||
"pt": "Tamanho máximo do histórico do condensador de memória",
|
||||
"es": "Tamaño máximo del historial del condensador de memoria",
|
||||
"ar": "الحد الأقصى لحجم سجل مكثف الذاكرة",
|
||||
"fr": "Taille maximale de l'historique du condenseur de mémoire",
|
||||
"tr": "Bellek yoğunlaştırıcı maksimum geçmiş boyutu",
|
||||
"de": "Maximale Verlaufgröße des Speicherkondensators",
|
||||
"uk": "Максимальний розмір історії конденсатора пам'яті"
|
||||
},
|
||||
"SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP": {
|
||||
"en": "After this many events, the condenser will summarize history. Minimum 10.",
|
||||
"ja": "このイベント数を超えると、凝縮器が履歴を要約します。最小 10。",
|
||||
"zh-CN": "达到此事件数量后,凝缩器将汇总历史。最小 10。",
|
||||
"zh-TW": "超過此事件數後,凝縮器會摘要歷史。最小 10。",
|
||||
"ko-KR": "이 이벤트 수 이후 응축기가 기록을 요약합니다. 최소 10.",
|
||||
"no": "Etter så mange hendelser vil kondenseren oppsummere historikken. Minimum 10.",
|
||||
"it": "Dopo questo numero di eventi, il condensatore riassumerà la cronologia. Minimo 10.",
|
||||
"pt": "Após esse número de eventos, o condensador irá resumir o histórico. Mínimo 10.",
|
||||
"es": "Después de este número de eventos, el condensador resumirá el historial. Mínimo 10.",
|
||||
"ar": "بعد هذا العدد من الأحداث، سيقوم المكثف بتلخيص السجل. الحد الأدنى 10.",
|
||||
"fr": "Après ce nombre d'événements, le condenseur résumera l'historique. Minimum 10.",
|
||||
"tr": "Bu kadar olaydan sonra yoğunlaştırıcı geçmişi özetler. En az 10.",
|
||||
"de": "Nach so vielen Ereignissen fasst der Kondensator die Historie zusammen. Minimum 10.",
|
||||
"uk": "Після цієї кількості подій конденсатор узагальнить історію. Мінімум 10."
|
||||
},
|
||||
"SETTINGS$LANGUAGE": {
|
||||
"en": "Language",
|
||||
"ja": "言語",
|
||||
@@ -2063,22 +2095,6 @@
|
||||
"de": "Git-Anbieter",
|
||||
"uk": "Git-провайдер"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$TITLE": {
|
||||
"en": "Account Settings",
|
||||
"ja": "アカウント設定",
|
||||
"zh-CN": "账户设置",
|
||||
"zh-TW": "帳戶設定",
|
||||
"ko-KR": "계정 설정",
|
||||
"no": "Kontoinnstillinger",
|
||||
"it": "Impostazioni account",
|
||||
"pt": "Configurações da conta",
|
||||
"es": "Configuración de la cuenta",
|
||||
"ar": "إعدادات الحساب",
|
||||
"fr": "Paramètres du compte",
|
||||
"tr": "Hesap ayarları",
|
||||
"de": "Kontoeinstellungen",
|
||||
"uk": "Налаштування облікового запису"
|
||||
},
|
||||
"WORKSPACE$TERMINAL_TAB_LABEL": {
|
||||
"en": "Terminal",
|
||||
"zh-CN": "终端",
|
||||
@@ -5215,6 +5231,22 @@
|
||||
"tr": "Durdur",
|
||||
"uk": "Стоп"
|
||||
},
|
||||
"BUTTON$PAUSE": {
|
||||
"en": "Pause",
|
||||
"ja": "一時停止",
|
||||
"zh-CN": "暂停",
|
||||
"zh-TW": "暫停",
|
||||
"ko-KR": "일시정지",
|
||||
"fr": "Mettre en pause",
|
||||
"es": "Pausar",
|
||||
"de": "Pausieren",
|
||||
"it": "Pausa",
|
||||
"pt": "Pausar",
|
||||
"ar": "إيقاف مؤقت",
|
||||
"no": "Pause",
|
||||
"tr": "Duraklat",
|
||||
"uk": "Призупинити"
|
||||
},
|
||||
"BUTTON$EDIT_TITLE": {
|
||||
"en": "Edit Title",
|
||||
"ja": "タイトルを編集",
|
||||
@@ -5391,8 +5423,40 @@
|
||||
"de": "Stopp bestätigen",
|
||||
"uk": "Підтвердити зупинку"
|
||||
},
|
||||
"CONVERSATION$CONFIRM_PAUSE": {
|
||||
"en": "Confirm Pause",
|
||||
"ja": "一時停止の確認",
|
||||
"zh-CN": "确认暂停",
|
||||
"zh-TW": "確認暫停",
|
||||
"ko-KR": "일시정지 확인",
|
||||
"no": "Bekreft pause",
|
||||
"it": "Conferma pausa",
|
||||
"pt": "Confirmar pausa",
|
||||
"es": "Confirmar pausa",
|
||||
"ar": "تأكيد الإيقاف المؤقت",
|
||||
"fr": "Confirmer la mise en pause",
|
||||
"tr": "Duraklatmayı Onayla",
|
||||
"de": "Pause bestätigen",
|
||||
"uk": "Підтвердити призупинення"
|
||||
},
|
||||
"CONVERSATION$PAUSE_WARNING": {
|
||||
"en": "Are you sure you want to pause this conversation?",
|
||||
"ja": "この会話を一時停止してもよろしいですか?",
|
||||
"zh-CN": "您确定要暂停此对话吗?",
|
||||
"zh-TW": "您確定要暫停此對話嗎?",
|
||||
"ko-KR": "이 대화를 일시정지하시겠습니까?",
|
||||
"no": "Er du sikker på at du vil pause denne samtalen?",
|
||||
"it": "Sei sicuro di voler mettere in pausa questa conversazione?",
|
||||
"pt": "Tem certeza de que deseja pausar esta conversa?",
|
||||
"es": "¿Está seguro de que desea pausar esta conversación?",
|
||||
"ar": "هل أنت متأكد أنك تريد إيقاف هذه المحادثة مؤقتًا؟",
|
||||
"fr": "Êtes-vous sûr de vouloir mettre cette conversation en pause ?",
|
||||
"tr": "Bu konuşmayı duraklatmak istediğinizden emin misiniz?",
|
||||
"de": "Sind Sie sicher, dass Sie dieses Gespräch pausieren möchten?",
|
||||
"uk": "Ви впевнені, що хочете призупинити цю розмову?"
|
||||
},
|
||||
"CONVERSATION$STOP_WARNING": {
|
||||
"en": "Are you sure you want to stop this conversation?",
|
||||
"en": "Are you sure you want to pause this conversation?",
|
||||
"ja": "この会話を停止してもよろしいですか?",
|
||||
"zh-CN": "您确定要停止此对话吗?",
|
||||
"zh-TW": "您確定要停止此對話嗎?",
|
||||
@@ -7583,22 +7647,6 @@
|
||||
"tr": "Ajan görevine devam et",
|
||||
"uk": "Відновити завдання агента"
|
||||
},
|
||||
"ACTION_BUTTON$PAUSE": {
|
||||
"en": "Pause the current task",
|
||||
"zh-CN": "暂停",
|
||||
"zh-TW": "暫停",
|
||||
"ko-KR": "일시정지",
|
||||
"ja": "一時停止",
|
||||
"no": "Sett gjeldende oppgave på pause",
|
||||
"ar": "إيقاف المهمة الحالية مؤقتاً",
|
||||
"de": "Aktuelle Aufgabe pausieren",
|
||||
"fr": "Mettre en pause la tâche actuelle",
|
||||
"it": "Metti in pausa il compito corrente",
|
||||
"pt": "Pausar a tarefa atual",
|
||||
"es": "Pausar la tarea actual",
|
||||
"tr": "Mevcut görevi duraklat",
|
||||
"uk": "Призупинити поточне завдання"
|
||||
},
|
||||
"BROWSER$SCREENSHOT_ALT": {
|
||||
"en": "Browser Screenshot",
|
||||
"zh-CN": "截图",
|
||||
@@ -8207,22 +8255,6 @@
|
||||
"tr": "Kullanıcı avatarı yer tutucusu",
|
||||
"uk": "заповнювач аватара користувача"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$SETTINGS": {
|
||||
"en": "Account Settings",
|
||||
"ja": "アカウント設定",
|
||||
"zh-CN": "账户设置",
|
||||
"zh-TW": "帳戶設定",
|
||||
"ko-KR": "계정 설정",
|
||||
"no": "Kontoinnstillinger",
|
||||
"it": "Impostazioni account",
|
||||
"pt": "Configurações da conta",
|
||||
"es": "Configuración de la cuenta",
|
||||
"ar": "إعدادات الحساب",
|
||||
"fr": "Paramètres du compte",
|
||||
"tr": "Hesap ayarları",
|
||||
"de": "Kontoeinstellungen",
|
||||
"uk": "Налаштування облікового запису"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$LOGOUT": {
|
||||
"en": "Logout",
|
||||
"ja": "ログアウト",
|
||||
@@ -9167,38 +9199,6 @@
|
||||
"tr": "Bekleme listesine katıl",
|
||||
"uk": "Приєднатися до списку очікування"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$ADDITIONAL_SETTINGS": {
|
||||
"en": "Additional Settings",
|
||||
"ja": "追加設定",
|
||||
"zh-CN": "附加设置",
|
||||
"zh-TW": "附加設定",
|
||||
"ko-KR": "추가 설정",
|
||||
"de": "Zusätzliche Einstellungen",
|
||||
"no": "Ytterligere innstillinger",
|
||||
"it": "Impostazioni aggiuntive",
|
||||
"pt": "Configurações adicionais",
|
||||
"es": "Configuraciones adicionales",
|
||||
"ar": "إعدادات إضافية",
|
||||
"fr": "Paramètres supplémentaires",
|
||||
"tr": "Ek Ayarlar",
|
||||
"uk": "Додаткові налаштування"
|
||||
},
|
||||
"ACCOUNT_SETTINGS$DISCONNECT_FROM_GITHUB": {
|
||||
"en": "Disconnect from GitHub",
|
||||
"ja": "GitHubから切断",
|
||||
"zh-CN": "断开与GitHub的连接",
|
||||
"zh-TW": "中斷與GitHub的連接",
|
||||
"ko-KR": "GitHub 연결 해제",
|
||||
"de": "Von GitHub trennen",
|
||||
"no": "Koble fra GitHub",
|
||||
"it": "Disconnetti da GitHub",
|
||||
"pt": "Desconectar do GitHub",
|
||||
"es": "Desconectar de GitHub",
|
||||
"ar": "قطع الاتصال من GitHub",
|
||||
"fr": "Se déconnecter de GitHub",
|
||||
"tr": "GitHub'dan bağlantıyı kes",
|
||||
"uk": "Відключитися від GitHub"
|
||||
},
|
||||
"CONVERSATION$DELETE_WARNING": {
|
||||
"en": "Are you sure you want to delete this conversation? This action cannot be undone.",
|
||||
"ja": "この会話を削除してもよろしいですか?この操作は元に戻せません。",
|
||||
|
||||
@@ -27,6 +27,7 @@ export const MOCK_DEFAULT_USER_SETTINGS: ApiSettings | PostApiSettings = {
|
||||
DEFAULT_SETTINGS.REMOTE_RUNTIME_RESOURCE_FACTOR,
|
||||
provider_tokens_set: {},
|
||||
enable_default_condenser: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER,
|
||||
condenser_max_size: DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
|
||||
enable_sound_notifications: DEFAULT_SETTINGS.ENABLE_SOUND_NOTIFICATIONS,
|
||||
enable_proactive_conversation_starters:
|
||||
DEFAULT_SETTINGS.ENABLE_PROACTIVE_CONVERSATION_STARTERS,
|
||||
@@ -198,7 +199,14 @@ export const handlers = [
|
||||
const body = await request.json();
|
||||
|
||||
if (body) {
|
||||
MOCK_USER_PREFERENCES.settings = MOCK_DEFAULT_USER_SETTINGS;
|
||||
const current = MOCK_USER_PREFERENCES.settings || {
|
||||
...MOCK_DEFAULT_USER_SETTINGS,
|
||||
};
|
||||
// Persist new values over current/mock defaults
|
||||
MOCK_USER_PREFERENCES.settings = {
|
||||
...current,
|
||||
...(body as Partial<ApiSettings>),
|
||||
};
|
||||
return HttpResponse.json(null, { status: 200 });
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ function LlmSettingsScreen() {
|
||||
confirmationMode: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
});
|
||||
|
||||
// Track the currently selected model to show help text
|
||||
@@ -124,6 +125,7 @@ function LlmSettingsScreen() {
|
||||
confirmationMode: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -181,6 +183,13 @@ function LlmSettingsScreen() {
|
||||
formData.get("enable-confirmation-mode-switch")?.toString() === "on";
|
||||
const enableDefaultCondenser =
|
||||
formData.get("enable-memory-condenser-switch")?.toString() === "on";
|
||||
const condenserMaxSizeStr = formData
|
||||
.get("condenser-max-size-input")
|
||||
?.toString();
|
||||
const condenserMaxSize = condenserMaxSizeStr
|
||||
? Number.parseInt(condenserMaxSizeStr, 10)
|
||||
: undefined;
|
||||
|
||||
const securityAnalyzer = formData
|
||||
.get("security-analyzer-input")
|
||||
?.toString();
|
||||
@@ -194,6 +203,8 @@ function LlmSettingsScreen() {
|
||||
AGENT: agent,
|
||||
CONFIRMATION_MODE: confirmationMode,
|
||||
ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser,
|
||||
CONDENSER_MAX_SIZE:
|
||||
condenserMaxSize ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE,
|
||||
SECURITY_ANALYZER:
|
||||
securityAnalyzer === "none"
|
||||
? null
|
||||
@@ -222,6 +233,7 @@ function LlmSettingsScreen() {
|
||||
confirmationMode: false,
|
||||
enableDefaultCondenser: false,
|
||||
securityAnalyzer: false,
|
||||
condenserMaxSize: false,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -308,6 +320,17 @@ function LlmSettingsScreen() {
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCondenserMaxSizeIsDirty = (value: string) => {
|
||||
const parsed = value ? Number.parseInt(value, 10) : undefined;
|
||||
const condenserMaxSizeIsDirty =
|
||||
(parsed ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE) !==
|
||||
(settings?.CONDENSER_MAX_SIZE ?? DEFAULT_SETTINGS.CONDENSER_MAX_SIZE);
|
||||
setDirtyInputs((prev) => ({
|
||||
...prev,
|
||||
condenserMaxSize: condenserMaxSizeIsDirty,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSecurityAnalyzerIsDirty = (securityAnalyzer: string) => {
|
||||
const securityAnalyzerIsDirty =
|
||||
securityAnalyzer !== settings?.SECURITY_ANALYZER;
|
||||
@@ -565,6 +588,26 @@ function LlmSettingsScreen() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="w-full max-w-[680px]">
|
||||
<SettingsInput
|
||||
testId="condenser-max-size-input"
|
||||
name="condenser-max-size-input"
|
||||
type="number"
|
||||
min={10}
|
||||
step={1}
|
||||
label={t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE)}
|
||||
defaultValue={(
|
||||
settings.CONDENSER_MAX_SIZE ??
|
||||
DEFAULT_SETTINGS.CONDENSER_MAX_SIZE
|
||||
)?.toString()}
|
||||
onChange={(value) => handleCondenserMaxSizeIsDirty(value)}
|
||||
isDisabled={!settings.ENABLE_DEFAULT_CONDENSER}
|
||||
/>
|
||||
<p className="text-xs text-tertiary-alt mt-1">
|
||||
{t(I18nKey.SETTINGS$CONDENSER_MAX_SIZE_TOOLTIP)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingsSwitch
|
||||
testId="enable-memory-condenser-switch"
|
||||
name="enable-memory-condenser-switch"
|
||||
|
||||
@@ -14,6 +14,7 @@ export const DEFAULT_SETTINGS: Settings = {
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: 1,
|
||||
PROVIDER_TOKENS_SET: {},
|
||||
ENABLE_DEFAULT_CONDENSER: true,
|
||||
CONDENSER_MAX_SIZE: 120,
|
||||
ENABLE_SOUND_NOTIFICATIONS: false,
|
||||
USER_CONSENTS_TO_ANALYTICS: false,
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS: false,
|
||||
|
||||
@@ -1 +1,6 @@
|
||||
export type ConversationStatus = "STARTING" | "RUNNING" | "STOPPED";
|
||||
export type ConversationStatus =
|
||||
| "STARTING"
|
||||
| "RUNNING"
|
||||
| "STOPPED"
|
||||
| "ARCHIVED"
|
||||
| "ERROR";
|
||||
|
||||
@@ -47,6 +47,8 @@ export type Settings = {
|
||||
REMOTE_RUNTIME_RESOURCE_FACTOR: number | null;
|
||||
PROVIDER_TOKENS_SET: Partial<Record<Provider, string | null>>;
|
||||
ENABLE_DEFAULT_CONDENSER: boolean;
|
||||
// Maximum number of events before the condenser runs
|
||||
CONDENSER_MAX_SIZE: number | null;
|
||||
ENABLE_SOUND_NOTIFICATIONS: boolean;
|
||||
ENABLE_PROACTIVE_CONVERSATION_STARTERS: boolean;
|
||||
ENABLE_SOLVABILITY_ANALYSIS: boolean;
|
||||
@@ -73,6 +75,8 @@ export type ApiSettings = {
|
||||
security_analyzer: string | null;
|
||||
remote_runtime_resource_factor: number | null;
|
||||
enable_default_condenser: boolean;
|
||||
// Max size for condenser in backend settings
|
||||
condenser_max_size: number | null;
|
||||
enable_sound_notifications: boolean;
|
||||
enable_proactive_conversation_starters: boolean;
|
||||
enable_solvability_analysis: boolean;
|
||||
|
||||
@@ -24,12 +24,14 @@ export const VERIFIED_MODELS = [
|
||||
"kimi-k2-0711-preview",
|
||||
"qwen3-coder-480b",
|
||||
"gpt-5-2025-08-07",
|
||||
"gpt-5-mini-2025-08-07",
|
||||
];
|
||||
|
||||
// LiteLLM does not return OpenAI models with the provider, so we list them here to set them ourselves for consistency
|
||||
// (e.g., they return `gpt-4o` instead of `openai/gpt-4o`)
|
||||
export const VERIFIED_OPENAI_MODELS = [
|
||||
"gpt-5-2025-08-07",
|
||||
"gpt-5-mini-2025-08-07",
|
||||
"gpt-4o",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4.1",
|
||||
@@ -66,6 +68,7 @@ export const VERIFIED_MISTRAL_MODELS = [
|
||||
export const VERIFIED_OPENHANDS_MODELS = [
|
||||
"claude-sonnet-4-20250514",
|
||||
"gpt-5-2025-08-07",
|
||||
"gpt-5-mini-2025-08-07",
|
||||
"claude-opus-4-20250514",
|
||||
"claude-opus-4-1-20250805",
|
||||
"gemini-2.5-pro",
|
||||
|
||||
@@ -151,6 +151,7 @@ VERIFIED_PROVIDERS = ['openhands', 'anthropic', 'openai', 'mistral']
|
||||
|
||||
VERIFIED_OPENAI_MODELS = [
|
||||
'gpt-5-2025-08-07',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
'o4-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
@@ -186,6 +187,7 @@ VERIFIED_MISTRAL_MODELS = [
|
||||
VERIFIED_OPENHANDS_MODELS = [
|
||||
'claude-sonnet-4-20250514',
|
||||
'gpt-5-2025-08-07',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
'claude-opus-4-20250514',
|
||||
'claude-opus-4-1-20250805',
|
||||
'devstral-small-2507',
|
||||
|
||||
@@ -2,6 +2,8 @@ import os
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, model_validator
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class SandboxConfig(BaseModel):
|
||||
"""Configuration for the sandbox.
|
||||
@@ -55,6 +57,7 @@ class SandboxConfig(BaseModel):
|
||||
)
|
||||
runtime_container_image: str | None = Field(default=None)
|
||||
user_id: int = Field(default=os.getuid() if hasattr(os, 'getuid') else 1000)
|
||||
logger.debug(f'SandboxConfig user_id default: {user_id}')
|
||||
timeout: int = Field(default=120)
|
||||
remote_runtime_init_timeout: int = Field(default=180)
|
||||
remote_runtime_api_timeout: int = Field(default=10)
|
||||
|
||||
@@ -198,18 +198,12 @@ class ProviderHandler:
|
||||
|
||||
if selected_provider:
|
||||
if not page or not per_page:
|
||||
logger.error('Failed to provider params for paginating repos')
|
||||
return []
|
||||
raise ValueError('Failed to provider params for paginating repos')
|
||||
|
||||
service = self._get_service(selected_provider)
|
||||
try:
|
||||
return await service.get_paginated_repos(
|
||||
page, per_page, sort, installation_id
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f'Error fetching repos from {selected_provider}: {e}')
|
||||
|
||||
return []
|
||||
return await service.get_paginated_repos(
|
||||
page, per_page, sort, installation_id
|
||||
)
|
||||
|
||||
all_repos: list[Repository] = []
|
||||
for provider in self.provider_tokens:
|
||||
@@ -246,17 +240,10 @@ class ProviderHandler:
|
||||
if selected_provider:
|
||||
service = self._get_service(selected_provider)
|
||||
public = self._is_repository_url(query, selected_provider)
|
||||
try:
|
||||
user_repos = await service.search_repositories(
|
||||
query, per_page, sort, order, public
|
||||
)
|
||||
return self._deduplicate_repositories(user_repos)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f'Error searching repos from select provider {selected_provider}: {e}'
|
||||
)
|
||||
|
||||
return []
|
||||
user_repos = await service.search_repositories(
|
||||
query, per_page, sort, order, public
|
||||
)
|
||||
return self._deduplicate_repositories(user_repos)
|
||||
|
||||
all_repos: list[Repository] = []
|
||||
for provider in self.provider_tokens:
|
||||
|
||||
@@ -166,6 +166,17 @@ class LLM(RetryMixin, DebugMixin):
|
||||
elif 'gemini' in self.config.model.lower() and self.config.safety_settings:
|
||||
kwargs['safety_settings'] = self.config.safety_settings
|
||||
|
||||
# support AWS Bedrock provider
|
||||
kwargs['aws_region_name'] = self.config.aws_region_name
|
||||
if self.config.aws_access_key_id:
|
||||
kwargs['aws_access_key_id'] = (
|
||||
self.config.aws_access_key_id.get_secret_value()
|
||||
)
|
||||
if self.config.aws_secret_access_key:
|
||||
kwargs['aws_secret_access_key'] = (
|
||||
self.config.aws_secret_access_key.get_secret_value()
|
||||
)
|
||||
|
||||
# Explicitly disable Anthropic extended thinking for Opus 4.1 to avoid
|
||||
# requiring 'thinking' content blocks. See issue #10510.
|
||||
if 'claude-opus-4-1' in self.config.model.lower():
|
||||
|
||||
@@ -100,6 +100,7 @@ REASONING_EFFORT_PATTERNS: list[str] = [
|
||||
'gemini-2.5-pro',
|
||||
'gpt-5',
|
||||
'gpt-5-2025-08-07',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
# DeepSeek reasoning family
|
||||
'deepseek-r1-0528*',
|
||||
]
|
||||
|
||||
@@ -646,8 +646,10 @@ class ActionExecutor:
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logger.warning('Starting Action Execution Server')
|
||||
|
||||
logger.debug('Starting Action Execution Server')
|
||||
logger.debug('Arguments passed to script:')
|
||||
for i, arg in enumerate(sys.argv):
|
||||
logger.debug(f'Argument {i}: {arg}')
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('port', type=int, help='Port to listen on')
|
||||
parser.add_argument('--working-dir', type=str, help='Working directory')
|
||||
|
||||
@@ -76,6 +76,7 @@ class RemoteRuntime(ActionExecutionClient):
|
||||
user_id,
|
||||
git_provider_tokens,
|
||||
)
|
||||
logger.debug(f'RemoteRuntime.init user_id {user_id}')
|
||||
if self.config.sandbox.api_key is None:
|
||||
raise ValueError(
|
||||
'API key is required to use the remote runtime. '
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import traceback
|
||||
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
|
||||
DEFAULT_PYTHON_PREFIX = [
|
||||
@@ -23,6 +26,9 @@ def get_action_execution_server_startup_command(
|
||||
python_executable: str = 'python',
|
||||
) -> list[str]:
|
||||
sandbox_config = app_config.sandbox
|
||||
logger.debug(f'app_config {vars(app_config)}')
|
||||
logger.debug(f'sandbox_config {vars(sandbox_config)}')
|
||||
logger.debug(f'override_user_id {override_user_id}')
|
||||
|
||||
# Plugin args
|
||||
plugin_args = []
|
||||
@@ -39,9 +45,7 @@ def get_action_execution_server_startup_command(
|
||||
username = override_username or (
|
||||
'openhands' if app_config.run_as_openhands else 'root'
|
||||
)
|
||||
user_id = override_user_id or (
|
||||
sandbox_config.user_id if app_config.run_as_openhands else 0
|
||||
)
|
||||
user_id = override_user_id or (1000 if app_config.run_as_openhands else 0)
|
||||
|
||||
base_cmd = [
|
||||
*python_prefix,
|
||||
@@ -62,5 +66,10 @@ def get_action_execution_server_startup_command(
|
||||
|
||||
if not app_config.enable_browser:
|
||||
base_cmd.append('--no-enable-browser')
|
||||
logger.debug(f'get_action_execution_server_startup_command: {base_cmd}')
|
||||
logger.debug(
|
||||
'get_action_execution_server_startup_command stack:\n%s',
|
||||
''.join(traceback.format_stack()),
|
||||
)
|
||||
|
||||
return base_cmd
|
||||
|
||||
@@ -49,6 +49,61 @@ def init_user_and_working_directory(
|
||||
if username == os.getenv('USER') and username not in ['root', 'openhands']:
|
||||
return None
|
||||
|
||||
# Skip root since it is already created
|
||||
if username != 'root':
|
||||
# Check if the username already exists
|
||||
logger.debug(f'Attempting to create user `{username}` with UID {user_id}.')
|
||||
existing_user_id = -1
|
||||
try:
|
||||
result = subprocess.run(
|
||||
f'id -u {username}', shell=True, check=True, capture_output=True
|
||||
)
|
||||
existing_user_id = int(result.stdout.decode().strip())
|
||||
|
||||
# The user ID already exists, skip setup
|
||||
if existing_user_id == user_id:
|
||||
logger.debug(
|
||||
f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.'
|
||||
)
|
||||
return existing_user_id
|
||||
return None
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Returncode 1 indicates, that the user does not exist yet
|
||||
if e.returncode == 1:
|
||||
logger.debug(
|
||||
f'User `{username}` does not exist. Proceeding with user creation.'
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f'Error checking user `{username}`, skipping setup:\n{e}\n'
|
||||
)
|
||||
raise
|
||||
|
||||
# Add sudoer
|
||||
sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
|
||||
output = subprocess.run(sudoer_line, shell=True, capture_output=True)
|
||||
if output.returncode != 0:
|
||||
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
|
||||
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
|
||||
|
||||
command = (
|
||||
f'useradd -rm -d /home/{username} -s /bin/bash '
|
||||
f'-g root -G sudo -u {user_id} {username}'
|
||||
)
|
||||
output = subprocess.run(command, shell=True, capture_output=True)
|
||||
if output.returncode == 0:
|
||||
logger.debug(
|
||||
f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
|
||||
)
|
||||
|
||||
# First create the working directory, independent of the user
|
||||
logger.debug(f'Client working directory: {initial_cwd}')
|
||||
command = f'umask 002; mkdir -p {initial_cwd}'
|
||||
@@ -64,57 +119,4 @@ def init_user_and_working_directory(
|
||||
out_str += output.stdout.decode()
|
||||
logger.debug(f'Created working directory. Output: [{out_str}]')
|
||||
|
||||
# Skip root since it is already created
|
||||
if username == 'root':
|
||||
return None
|
||||
|
||||
# Check if the username already exists
|
||||
existing_user_id = -1
|
||||
try:
|
||||
result = subprocess.run(
|
||||
f'id -u {username}', shell=True, check=True, capture_output=True
|
||||
)
|
||||
existing_user_id = int(result.stdout.decode().strip())
|
||||
|
||||
# The user ID already exists, skip setup
|
||||
if existing_user_id == user_id:
|
||||
logger.debug(
|
||||
f'User `{username}` already has the provided UID {user_id}. Skipping user setup.'
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f'User `{username}` already exists with UID {existing_user_id}. Skipping user setup.'
|
||||
)
|
||||
return existing_user_id
|
||||
return None
|
||||
except subprocess.CalledProcessError as e:
|
||||
# Returncode 1 indicates, that the user does not exist yet
|
||||
if e.returncode == 1:
|
||||
logger.debug(
|
||||
f'User `{username}` does not exist. Proceeding with user creation.'
|
||||
)
|
||||
else:
|
||||
logger.error(f'Error checking user `{username}`, skipping setup:\n{e}\n')
|
||||
raise
|
||||
|
||||
# Add sudoer
|
||||
sudoer_line = r"echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers"
|
||||
output = subprocess.run(sudoer_line, shell=True, capture_output=True)
|
||||
if output.returncode != 0:
|
||||
raise RuntimeError(f'Failed to add sudoer: {output.stderr.decode()}')
|
||||
logger.debug(f'Added sudoer successfully. Output: [{output.stdout.decode()}]')
|
||||
|
||||
command = (
|
||||
f'useradd -rm -d /home/{username} -s /bin/bash '
|
||||
f'-g root -G sudo -u {user_id} {username}'
|
||||
)
|
||||
output = subprocess.run(command, shell=True, capture_output=True)
|
||||
if output.returncode == 0:
|
||||
logger.debug(
|
||||
f'Added user `{username}` successfully with UID {user_id}. Output: [{output.stdout.decode()}]'
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f'Failed to create user `{username}` with UID {user_id}. Output: [{output.stderr.decode()}]'
|
||||
)
|
||||
return None
|
||||
|
||||
@@ -14,12 +14,16 @@ ENV POETRY_VIRTUALENVS_PATH=/openhands/poetry \
|
||||
|
||||
{% macro setup_base_system() %}
|
||||
|
||||
# Set PATH early to ensure system commands are available
|
||||
ENV PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||||
|
||||
# Install base system dependencies
|
||||
|
||||
{% if (('ubuntu' in base_image) or ('mswebench' in base_image)) %}
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
|
||||
coreutils util-linux procps findutils grep sed \
|
||||
{%- if (base_image.endswith(':latest') or base_image.endswith(':24.04') or ('mswebench' in base_image)) -%}
|
||||
libgl1 \
|
||||
{%- else %}
|
||||
@@ -41,6 +45,7 @@ RUN apt-get update && \
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
wget curl ca-certificates sudo apt-utils git jq tmux build-essential ripgrep ffmpeg \
|
||||
coreutils util-linux procps findutils grep sed \
|
||||
libgl1-mesa-glx \
|
||||
libasound2-plugins libatomic1 \
|
||||
# Install Docker dependencies
|
||||
@@ -58,15 +63,30 @@ RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR="/openhands/
|
||||
# Add /openhands/bin to PATH
|
||||
ENV PATH="/openhands/bin:${PATH}"
|
||||
|
||||
# Remove UID 1000 named pn or ubuntu, so the 'openhands' user can be created from ubuntu hosts
|
||||
# Remove UID 1000 and GID 1000 users/groups that might conflict with openhands user
|
||||
RUN (if getent passwd 1000 | grep -q pn; then userdel pn; fi) && \
|
||||
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi)
|
||||
(if getent passwd 1000 | grep -q ubuntu; then userdel ubuntu; fi) && \
|
||||
(if getent group 1000 | grep -q pn; then groupdel pn; fi) && \
|
||||
(if getent group 1000 | grep -q ubuntu; then groupdel ubuntu; fi)
|
||||
|
||||
# Create openhands group and user
|
||||
RUN groupadd -g 1000 openhands && \
|
||||
useradd -u 1000 -g 1000 -m -s /bin/bash openhands && \
|
||||
usermod -aG sudo openhands && \
|
||||
echo 'openhands ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
|
||||
# Set empty password for openhands user to allow passwordless su
|
||||
passwd -d openhands && \
|
||||
# Set empty password for root user as well to ensure su works in both directions
|
||||
passwd -d root && \
|
||||
# Ensure root can su to openhands without password by configuring PAM
|
||||
sed -i '/pam_rootok.so/d' /etc/pam.d/su && \
|
||||
sed -i '1i auth sufficient pam_rootok.so' /etc/pam.d/su
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /openhands && \
|
||||
mkdir -p /openhands/logs && \
|
||||
mkdir -p /openhands/poetry
|
||||
mkdir -p /openhands/poetry && \
|
||||
chown -R openhands:openhands /openhands
|
||||
|
||||
|
||||
# ================================================================
|
||||
@@ -147,14 +167,16 @@ RUN if [ -z "${RELEASE_TAG}" ]; then \
|
||||
if [ -d "${OPENVSCODE_SERVER_ROOT}" ]; then rm -rf "${OPENVSCODE_SERVER_ROOT}"; fi && \
|
||||
mv ${RELEASE_TAG}-linux-${arch} ${OPENVSCODE_SERVER_ROOT} && \
|
||||
cp ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/openvscode-server ${OPENVSCODE_SERVER_ROOT}/bin/remote-cli/code && \
|
||||
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz
|
||||
rm -f ${RELEASE_TAG}-linux-${arch}.tar.gz && \
|
||||
chown -R openhands:openhands ${OPENVSCODE_SERVER_ROOT}
|
||||
|
||||
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro install_vscode_extensions() %}
|
||||
# Install our custom extension
|
||||
# Install our custom extensions as openhands user
|
||||
USER openhands
|
||||
RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world && \
|
||||
cp -r /openhands/code/openhands/runtime/utils/vscode-extensions/hello-world/* ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-hello-world/
|
||||
|
||||
@@ -165,27 +187,72 @@ RUN mkdir -p ${OPENVSCODE_SERVER_ROOT}/extensions/openhands-memory-monitor && \
|
||||
RUN rm -rf ${OPENVSCODE_SERVER_ROOT}/extensions/{handlebars,pug,json,diff,grunt,ini,npm}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro install_dependencies() %}
|
||||
# Install all dependencies
|
||||
{% macro install_dependencies_root() %}
|
||||
# Install system-level dependencies that require root
|
||||
USER root
|
||||
RUN \
|
||||
{% if enable_browser %}
|
||||
# Install system dependencies for Playwright (requires root)
|
||||
apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
libnss3 libnspr4 libatk-bridge2.0-0 libdrm2 libxkbcommon0 libxcomposite1 \
|
||||
libxdamage1 libxrandr2 libgbm1 libxss1 && \
|
||||
# Install libasound2 - try new package name first (Ubuntu 24.04+), fallback to old name
|
||||
(apt-get install -y --no-install-recommends libasound2t64 || apt-get install -y --no-install-recommends libasound2) && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* && \
|
||||
# Install Playwright browsers in shared location accessible to all users
|
||||
export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers && \
|
||||
mkdir -p /opt/playwright-browsers && \
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install chromium && \
|
||||
# Set proper permissions for shared access
|
||||
chmod -R 755 /opt/playwright-browsers && \
|
||||
# Create cache directories and symlinks for both users
|
||||
mkdir -p /home/openhands/.cache && \
|
||||
mkdir -p /root/.cache && \
|
||||
ln -sf /opt/playwright-browsers /home/openhands/.cache/ms-playwright && \
|
||||
ln -sf /opt/playwright-browsers /root/.cache/ms-playwright && \
|
||||
chown -h openhands:openhands /home/openhands/.cache/ms-playwright && \
|
||||
# Set environment variable for all users
|
||||
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /etc/environment && \
|
||||
{% endif %}
|
||||
# Set environment variables (requires root)
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
|
||||
# Set permissions for shared read-only access
|
||||
chmod -R 755 /openhands/poetry && \
|
||||
chmod -R 755 /openhands/micromamba && \
|
||||
chown -R openhands:openhands /openhands/poetry && \
|
||||
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
|
||||
chown -R openhands:openhands /openhands/workspace && \
|
||||
chown -R openhands:openhands /openhands/micromamba && \
|
||||
# Ensure PATH includes system binaries early in startup
|
||||
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/environment && \
|
||||
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"' >> /etc/bash.bashrc && \
|
||||
# Set up conda environment activation for all users
|
||||
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /etc/bash.bashrc && \
|
||||
echo 'micromamba activate openhands 2>/dev/null || true' >> /etc/bash.bashrc && \
|
||||
# Set up environment for root user
|
||||
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /root/.bashrc && \
|
||||
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /root/.bashrc && \
|
||||
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /root/.bashrc && \
|
||||
echo 'micromamba activate openhands 2>/dev/null || true' >> /root/.bashrc && \
|
||||
# Clean up system packages (requires root)
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
{% endmacro %}
|
||||
|
||||
{% macro install_dependencies_user() %}
|
||||
# Install user-level dependencies as openhands user
|
||||
WORKDIR /openhands/code
|
||||
|
||||
USER openhands
|
||||
RUN \
|
||||
/openhands/micromamba/bin/micromamba config set changeps1 False && \
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && \
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry env use python3.12 && \
|
||||
# Install project dependencies
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry install --only main,runtime --no-interaction --no-root && \
|
||||
# Update and install additional tools
|
||||
# (There used to be an "apt-get update" here, hopefully we can skip it.)
|
||||
{% if enable_browser %}/openhands/micromamba/bin/micromamba run -n openhands poetry run playwright install --with-deps chromium && \{% endif %}
|
||||
# Set environment variables
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry run python -c "import sys; print('OH_INTERPRETER_PATH=' + sys.executable)" >> /etc/environment && \
|
||||
# Set permissions
|
||||
chmod -R g+rws /openhands/poetry && \
|
||||
mkdir -p /openhands/workspace && chmod -R g+rws,o+rw /openhands/workspace && \
|
||||
# Clean up
|
||||
# Clean up user caches
|
||||
/openhands/micromamba/bin/micromamba run -n openhands poetry cache clear --all . -n && \
|
||||
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
|
||||
/openhands/micromamba/bin/micromamba clean --all
|
||||
|
||||
{% endmacro %}
|
||||
@@ -203,7 +270,16 @@ RUN \
|
||||
RUN mkdir -p /openhands/micromamba/bin && \
|
||||
/bin/bash -c "PREFIX_LOCATION=/openhands/micromamba BIN_FOLDER=/openhands/micromamba/bin INIT_YES=no CONDA_FORGE_YES=yes $(curl -L https://micro.mamba.pm/install.sh)" && \
|
||||
/openhands/micromamba/bin/micromamba config remove channels defaults && \
|
||||
/openhands/micromamba/bin/micromamba config list
|
||||
/openhands/micromamba/bin/micromamba config list && \
|
||||
chown -R openhands:openhands /openhands/micromamba && \
|
||||
# Create read-only shared access to micromamba for all users
|
||||
# This allows both root and openhands users to access the same packages
|
||||
# while maintaining security by keeping openhands as the owner
|
||||
chmod -R 755 /openhands/micromamba && \
|
||||
# Create a separate writable location for root's micromamba cache/config
|
||||
mkdir -p /root/.local/share/micromamba && \
|
||||
# Set up environment variables for system-wide access
|
||||
echo 'export PATH="/openhands/micromamba/bin:$PATH"' >> /etc/environment
|
||||
|
||||
# Create the openhands virtual environment and install poetry and python
|
||||
RUN /openhands/micromamba/bin/micromamba create -n openhands -y && \
|
||||
@@ -214,40 +290,80 @@ RUN \
|
||||
if [ -d /openhands/code ]; then rm -rf /openhands/code; fi && \
|
||||
mkdir -p /openhands/code/openhands && \
|
||||
touch /openhands/code/openhands/__init__.py && \
|
||||
chown -R openhands:openhands /openhands/code && \
|
||||
# Set global git configuration to ensure proper author/committer information
|
||||
git config --global user.name "openhands" && \
|
||||
git config --global user.email "openhands@all-hands.dev"
|
||||
|
||||
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
|
||||
COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/
|
||||
|
||||
{{ install_dependencies() }}
|
||||
{{ install_dependencies_user() }}
|
||||
{{ install_dependencies_root() }}
|
||||
|
||||
# ================================================================
|
||||
# END: Build Runtime Image from Scratch
|
||||
# ================================================================
|
||||
{% endif %}
|
||||
|
||||
# Ensure openhands user/group and base dirs exist even when not building from scratch
|
||||
USER root
|
||||
RUN \
|
||||
# Remove existing user/group by name or UID/GID 1000
|
||||
if getent passwd openhands >/dev/null 2>&1; then userdel -r -f openhands || true; fi && \
|
||||
if getent passwd 1000 >/dev/null 2>&1; then userdel -r -f "$(getent passwd 1000 | cut -d: -f1)" || true; fi && \
|
||||
if getent group openhands >/dev/null 2>&1; then groupdel openhands || true; fi && \
|
||||
if getent group 1000 >/dev/null 2>&1; then groupdel "$(getent group 1000 | cut -d: -f1)" || true; fi && \
|
||||
\
|
||||
# Recreate group with GID 1000
|
||||
groupadd -g 1000 openhands && \
|
||||
\
|
||||
# Recreate user with UID 1000
|
||||
useradd -u 1000 -g openhands -m -s /bin/bash openhands && \
|
||||
\
|
||||
# Ensure home and required directories exist
|
||||
mkdir -p /home/openhands && \
|
||||
mkdir -p /openhands && \
|
||||
mkdir -p $(dirname ${OPENVSCODE_SERVER_ROOT}) && \
|
||||
\
|
||||
# Ensure ownership is correct
|
||||
chown -R openhands:openhands /home/openhands || true && \
|
||||
chown -R openhands:openhands /openhands || true
|
||||
|
||||
{{ setup_vscode_server() }}
|
||||
|
||||
# ================================================================
|
||||
# Copy Project source files
|
||||
# ================================================================
|
||||
RUN if [ -d /openhands/code/openhands ]; then rm -rf /openhands/code/openhands; fi
|
||||
COPY ./code/pyproject.toml ./code/poetry.lock /openhands/code/
|
||||
COPY --chown=openhands:openhands ./code/pyproject.toml ./code/poetry.lock /openhands/code/
|
||||
RUN if [ -d /openhands/code/microagents ]; then rm -rf /openhands/code/microagents; fi
|
||||
COPY ./code/microagents /openhands/code/microagents
|
||||
COPY ./code/openhands /openhands/code/openhands
|
||||
RUN chmod a+rwx /openhands/code/openhands/__init__.py
|
||||
|
||||
COPY --chown=openhands:openhands ./code/microagents /openhands/code/microagents
|
||||
COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands
|
||||
RUN chmod a+rwx /openhands/code/openhands/__init__.py && \
|
||||
chown -R openhands:openhands /openhands/code
|
||||
|
||||
|
||||
# ================================================================
|
||||
# END: Build from versioned image
|
||||
# ================================================================
|
||||
{% if build_from_versioned %}
|
||||
{{ install_dependencies() }}
|
||||
{{ install_dependencies_user() }}
|
||||
{{ install_dependencies_root() }}
|
||||
{{ install_vscode_extensions() }}
|
||||
{% endif %}
|
||||
|
||||
# Install extra dependencies if specified
|
||||
{% if extra_deps %}RUN {{ extra_deps }} {% endif %}
|
||||
# Install extra dependencies if specified (as openhands user)
|
||||
{% if extra_deps %}
|
||||
USER openhands
|
||||
RUN {{ extra_deps }}
|
||||
{% endif %}
|
||||
|
||||
# Set up environment for openhands user
|
||||
USER root
|
||||
RUN \
|
||||
# Set up environment for openhands user
|
||||
echo 'export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/openhands/micromamba/bin:$PATH"' >> /home/openhands/.bashrc && \
|
||||
echo 'export PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers' >> /home/openhands/.bashrc && \
|
||||
echo 'eval "$(/openhands/micromamba/bin/micromamba shell hook --shell bash)"' >> /home/openhands/.bashrc && \
|
||||
echo 'micromamba activate openhands 2>/dev/null || true' >> /home/openhands/.bashrc && \
|
||||
chown openhands:openhands /home/openhands/.bashrc
|
||||
|
||||
@@ -203,12 +203,15 @@ class Session:
|
||||
# The order matters: with the browser output first, the summarizer
|
||||
# will only see the most recent browser output, which should keep
|
||||
# the summarization cost down.
|
||||
max_events_for_condenser = settings.condenser_max_size or 120
|
||||
default_condenser_config = CondenserPipelineConfig(
|
||||
condensers=[
|
||||
ConversationWindowCondenserConfig(),
|
||||
BrowserOutputCondenserConfig(attention_window=2),
|
||||
LLMSummarizingCondenserConfig(
|
||||
llm_config=llm_config, keep_first=4, max_size=120
|
||||
llm_config=llm_config,
|
||||
keep_first=4,
|
||||
max_size=max_events_for_condenser,
|
||||
),
|
||||
]
|
||||
)
|
||||
@@ -218,7 +221,7 @@ class Session:
|
||||
f' browser_output_masking(attention_window=2), '
|
||||
f' llm(model="{llm_config.model}", '
|
||||
f' base_url="{llm_config.base_url}", '
|
||||
f' keep_first=4, max_size=80)'
|
||||
f' keep_first=4, max_size={max_events_for_condenser})'
|
||||
)
|
||||
agent_config.condenser = default_condenser_config
|
||||
agent = Agent.get_cls(agent_cls)(agent_config, self.llm_registry)
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
"""
|
||||
This class is similar to the RuntimeStatus defined in the runtime api. (When this class was defined
|
||||
a RuntimeStatus class already existed in OpenHands which serves a completely different purpose) Some of
|
||||
the status definitions do not match up:
|
||||
|
||||
STOPPED/paused - the runtime is not running but may be restarted
|
||||
ARCHIVED/stopped - the runtime is not running and will not restart due to deleted files.
|
||||
"""
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class ConversationStatus(Enum):
|
||||
# The conversation is starting
|
||||
STARTING = 'STARTING'
|
||||
# The conversation is running - the agent may be working or idle
|
||||
RUNNING = 'RUNNING'
|
||||
# The conversation has stopped (This is synonymous with `paused` in the runtime API.)
|
||||
STOPPED = 'STOPPED'
|
||||
# The conversation has been archived and cannot be restarted.
|
||||
ARCHIVED = 'ARCHIVED'
|
||||
# Something has gone wrong with the conversation (The runtime rather than the agent)
|
||||
ERROR = 'ERROR'
|
||||
|
||||
@@ -7,6 +7,7 @@ from pydantic import (
|
||||
SecretStr,
|
||||
SerializationInfo,
|
||||
field_serializer,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic.json import pydantic_encoder
|
||||
@@ -42,6 +43,8 @@ class Settings(BaseModel):
|
||||
search_api_key: SecretStr | None = None
|
||||
sandbox_api_key: SecretStr | None = None
|
||||
max_budget_per_task: float | None = None
|
||||
# Maximum number of events in the conversation view before condensation runs
|
||||
condenser_max_size: int | None = None
|
||||
email: str | None = None
|
||||
email_verified: bool | None = None
|
||||
git_user_name: str | None = None
|
||||
@@ -102,6 +105,15 @@ class Settings(BaseModel):
|
||||
data['secret_store'] = secret_store
|
||||
return data
|
||||
|
||||
@field_validator('condenser_max_size')
|
||||
@classmethod
|
||||
def validate_condenser_max_size(cls, v: int | None) -> int | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v < 10:
|
||||
raise ValueError('condenser_max_size must be at least 10')
|
||||
return v
|
||||
|
||||
@field_serializer('secrets_store')
|
||||
def secrets_store_serializer(self, secrets: UserSecrets, info: SerializationInfo):
|
||||
"""Custom serializer for secrets store."""
|
||||
|
||||
@@ -57,6 +57,7 @@ def get_supported_llm_models(config: OpenHandsConfig) -> list[str]:
|
||||
openhands_models = [
|
||||
'openhands/claude-sonnet-4-20250514',
|
||||
'openhands/gpt-5-2025-08-07',
|
||||
'openhands/gpt-5-mini-2025-08-07',
|
||||
'openhands/claude-opus-4-20250514',
|
||||
'openhands/gemini-2.5-pro',
|
||||
'openhands/o3',
|
||||
|
||||
@@ -86,6 +86,15 @@ def test_model_matches_provider_qualified(name, pattern, expected):
|
||||
supports_stop_words=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'gpt-5-mini-2025-08-07',
|
||||
ModelFeatures(
|
||||
supports_function_calling=True,
|
||||
supports_reasoning_effort=True,
|
||||
supports_prompt_cache=False,
|
||||
supports_stop_words=True,
|
||||
),
|
||||
),
|
||||
(
|
||||
'o3-mini',
|
||||
ModelFeatures(
|
||||
@@ -172,6 +181,7 @@ def test_get_features(model, expect):
|
||||
'gpt-4o',
|
||||
'gpt-4.1',
|
||||
'gpt-5',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
# o-series
|
||||
'o1-2024-12-17',
|
||||
'o3-mini',
|
||||
@@ -199,6 +209,7 @@ def test_function_calling_models(model):
|
||||
'gemini-2.5-flash',
|
||||
'gemini-2.5-pro',
|
||||
'gpt-5',
|
||||
'gpt-5-mini-2025-08-07',
|
||||
],
|
||||
)
|
||||
def test_reasoning_effort_models(model):
|
||||
@@ -252,6 +263,7 @@ def test_prompt_cache_models(model):
|
||||
('gemini-2.5-pro', True),
|
||||
('gpt-5', True),
|
||||
('gpt-5-2025-08-07', True),
|
||||
('gpt-5-mini-2025-08-07', True),
|
||||
('claude-opus-4-1-20250805', False),
|
||||
# DeepSeek
|
||||
('deepseek/DeepSeek-R1-0528:671b-Q4_K_XL', True),
|
||||
|
||||
@@ -166,7 +166,10 @@ def test_generate_dockerfile_build_from_scratch():
|
||||
assert 'python=3.12' in dockerfile_content
|
||||
|
||||
# Check the update command
|
||||
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
|
||||
assert (
|
||||
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
|
||||
in dockerfile_content
|
||||
)
|
||||
assert (
|
||||
'/openhands/micromamba/bin/micromamba run -n openhands poetry install'
|
||||
in dockerfile_content
|
||||
@@ -188,7 +191,10 @@ def test_generate_dockerfile_build_from_lock():
|
||||
assert 'poetry install' not in dockerfile_content
|
||||
|
||||
# These update commands SHOULD still in the dockerfile
|
||||
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
|
||||
assert (
|
||||
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
|
||||
def test_generate_dockerfile_build_from_versioned():
|
||||
@@ -206,7 +212,10 @@ def test_generate_dockerfile_build_from_versioned():
|
||||
|
||||
# this SHOULD exist when build from versioned
|
||||
assert 'poetry install' in dockerfile_content
|
||||
assert 'COPY ./code/openhands /openhands/code/openhands' in dockerfile_content
|
||||
assert (
|
||||
'COPY --chown=openhands:openhands ./code/openhands /openhands/code/openhands'
|
||||
in dockerfile_content
|
||||
)
|
||||
|
||||
|
||||
def test_get_runtime_image_repo_and_tag_eventstream():
|
||||
|
||||
Reference in New Issue
Block a user