diff --git a/.github/workflows/openhands-resolver.yml b/.github/workflows/openhands-resolver.yml index 45718be9ef3d..f0fed3ac70ad 100644 --- a/.github/workflows/openhands-resolver.yml +++ b/.github/workflows/openhands-resolver.yml @@ -84,7 +84,11 @@ jobs: run: | python -m pip index versions openhands-ai > openhands_versions.txt OPENHANDS_VERSION=$(head -n 1 openhands_versions.txt | awk '{print $2}' | tr -d '()') - echo -e "\nopenhands-ai==${OPENHANDS_VERSION}" >> requirements.txt + # Ensure requirements.txt ends with newline before appending + if [ -f requirements.txt ] && [ -s requirements.txt ]; then + sed -i -e '$a\' requirements.txt + fi + echo "openhands-ai==${OPENHANDS_VERSION}" >> requirements.txt cat requirements.txt - name: Cache pip dependencies diff --git a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx index 1f2ad6c57a3e..dbba5c079f03 100644 --- a/frontend/__tests__/components/features/sidebar/sidebar.test.tsx +++ b/frontend/__tests__/components/features/sidebar/sidebar.test.tsx @@ -155,7 +155,9 @@ describe("Sidebar", () => { const settingsModal = screen.getByTestId("ai-config-modal"); // Click the advanced options switch to show the API key input - const advancedOptionsSwitch = within(settingsModal).getByTestId("advanced-option-switch"); + const advancedOptionsSwitch = within(settingsModal).getByTestId( + "advanced-option-switch", + ); await user.click(advancedOptionsSwitch); const apiKeyInput = within(settingsModal).getByLabelText(/API\$KEY/i); diff --git a/frontend/__tests__/initial-query.test.tsx b/frontend/__tests__/initial-query.test.tsx index da499d98cd10..824921752973 100644 --- a/frontend/__tests__/initial-query.test.tsx +++ b/frontend/__tests__/initial-query.test.tsx @@ -1,20 +1,20 @@ import { describe, it, expect } from "vitest"; import store from "../src/store"; import { - setInitialQuery, - clearInitialQuery, + setInitialPrompt, + clearInitialPrompt, } from "../src/state/initial-query-slice"; describe("Initial Query Behavior", () => { - it("should clear initial query when clearInitialQuery is dispatched", () => { + it("should clear initial query when clearInitialPrompt is dispatched", () => { // Set up initial query in the store - store.dispatch(setInitialQuery("test query")); - expect(store.getState().initialQuery.initialQuery).toBe("test query"); + store.dispatch(setInitialPrompt("test query")); + expect(store.getState().initialQuery.initialPrompt).toBe("test query"); // Clear the initial query - store.dispatch(clearInitialQuery()); + store.dispatch(clearInitialPrompt()); // Verify initial query is cleared - expect(store.getState().initialQuery.initialQuery).toBeNull(); + expect(store.getState().initialQuery.initialPrompt).toBeNull(); }); }); diff --git a/frontend/src/api/open-hands.ts b/frontend/src/api/open-hands.ts index caef81a9a1a1..84d864854694 100644 --- a/frontend/src/api/open-hands.ts +++ b/frontend/src/api/open-hands.ts @@ -244,10 +244,14 @@ class OpenHands { static async createConversation( githubToken?: string, selectedRepository?: string, + initialUserMsg?: string, + imageUrls?: string[], ): Promise { const body = { github_token: githubToken, selected_repository: selectedRepository, + initial_user_msg: initialUserMsg, + image_urls: imageUrls, }; const { data } = await openHands.post( diff --git a/frontend/src/components/agent-status-map.constant.ts b/frontend/src/components/agent-status-map.constant.ts index d0d68f0a4760..ca5524e77239 100644 --- a/frontend/src/components/agent-status-map.constant.ts +++ b/frontend/src/components/agent-status-map.constant.ts @@ -23,7 +23,7 @@ export const AGENT_STATUS_MAP: { }, [AgentState.AWAITING_USER_INPUT]: { message: I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_INPUT_MESSAGE, - indicator: IndicatorColor.ORANGE, + indicator: IndicatorColor.BLUE, }, [AgentState.PAUSED]: { message: I18nKey.CHAT_INTERFACE$AGENT_PAUSED_MESSAGE, diff --git a/frontend/src/hooks/mutation/use-create-conversation.ts b/frontend/src/hooks/mutation/use-create-conversation.ts index 7d5abc7b5652..27c976c9457a 100644 --- a/frontend/src/hooks/mutation/use-create-conversation.ts +++ b/frontend/src/hooks/mutation/use-create-conversation.ts @@ -3,7 +3,7 @@ import { useNavigate } from "react-router"; import posthog from "posthog-js"; import { useDispatch, useSelector } from "react-redux"; import OpenHands from "#/api/open-hands"; -import { setInitialQuery } from "#/state/initial-query-slice"; +import { setInitialPrompt } from "#/state/initial-query-slice"; import { RootState } from "#/store"; import { useAuth } from "#/context/auth-context"; @@ -18,7 +18,7 @@ export const useCreateConversation = () => { ); return useMutation({ - mutationFn: (variables: { q?: string }) => { + mutationFn: async (variables: { q?: string }) => { if ( !variables.q?.trim() && !selectedRepository && @@ -28,10 +28,13 @@ export const useCreateConversation = () => { throw new Error("No query provided"); } - if (variables.q) dispatch(setInitialQuery(variables.q)); + if (variables.q) dispatch(setInitialPrompt(variables.q)); + return OpenHands.createConversation( gitHubToken || undefined, selectedRepository || undefined, + variables.q, + files, ); }, onSuccess: async ({ conversation_id: conversationId }, { q }) => { diff --git a/frontend/src/hooks/query/use-github-user.ts b/frontend/src/hooks/query/use-github-user.ts index 9841f6524cda..7d24e12abce0 100644 --- a/frontend/src/hooks/query/use-github-user.ts +++ b/frontend/src/hooks/query/use-github-user.ts @@ -6,7 +6,7 @@ import { useConfig } from "./use-config"; import OpenHands from "#/api/open-hands"; export const useGitHubUser = () => { - const { gitHubToken, setUserId } = useAuth(); + const { gitHubToken, setUserId, logout } = useAuth(); const { data: config } = useConfig(); const user = useQuery({ @@ -29,5 +29,11 @@ export const useGitHubUser = () => { } }, [user.data]); + React.useEffect(() => { + if (user.isError) { + logout(); + } + }, [user.isError]); + return user; }; diff --git a/frontend/src/mocks/handlers.ts b/frontend/src/mocks/handlers.ts index 9d1a42d4305d..d9c40ff146f7 100644 --- a/frontend/src/mocks/handlers.ts +++ b/frontend/src/mocks/handlers.ts @@ -141,7 +141,7 @@ export const handlers = [ { id: 2, full_name: "octocat/earth" }, ]), ), - http.get("https://api.github.com/user", () => { + http.get("/api/github/user", () => { const user: GitHubUser = { id: 1, login: "octocat", diff --git a/frontend/src/routes/_oh.app/event-handler.tsx b/frontend/src/routes/_oh.app/event-handler.tsx index 2c45f013e2f7..440eb704ab77 100644 --- a/frontend/src/routes/_oh.app/event-handler.tsx +++ b/frontend/src/routes/_oh.app/event-handler.tsx @@ -1,10 +1,8 @@ import React from "react"; -import { useWSStatusChange } from "./hooks/use-ws-status-change"; import { useHandleWSEvents } from "./hooks/use-handle-ws-events"; import { useHandleRuntimeActive } from "./hooks/use-handle-runtime-active"; export function EventHandler({ children }: React.PropsWithChildren) { - useWSStatusChange(); useHandleWSEvents(); useHandleRuntimeActive(); diff --git a/frontend/src/routes/_oh.app/hooks/use-ws-status-change.ts b/frontend/src/routes/_oh.app/hooks/use-ws-status-change.ts deleted file mode 100644 index 665dd1d4bf23..000000000000 --- a/frontend/src/routes/_oh.app/hooks/use-ws-status-change.ts +++ /dev/null @@ -1,68 +0,0 @@ -import React from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { - useWsClient, - WsClientProviderStatus, -} from "#/context/ws-client-provider"; -import { createChatMessage } from "#/services/chat-service"; -import { setCurrentAgentState } from "#/state/agent-slice"; -import { addUserMessage } from "#/state/chat-slice"; -import { clearFiles, clearInitialQuery } from "#/state/initial-query-slice"; -import { RootState } from "#/store"; -import { AgentState } from "#/types/agent-state"; - -export const useWSStatusChange = () => { - const { send, status } = useWsClient(); - const { curAgentState } = useSelector((state: RootState) => state.agent); - const dispatch = useDispatch(); - - const statusRef = React.useRef(null); - - const { files, initialQuery } = useSelector( - (state: RootState) => state.initialQuery, - ); - - const sendInitialQuery = (query: string, base64Files: string[]) => { - const timestamp = new Date().toISOString(); - send(createChatMessage(query, base64Files, timestamp)); - }; - - const dispatchInitialQuery = (query: string) => { - sendInitialQuery(query, files); - dispatch(clearFiles()); // reset selected files - dispatch(clearInitialQuery()); // reset initial query - }; - - const handleAgentInit = () => { - if (initialQuery) { - dispatchInitialQuery(initialQuery); - } - }; - React.useEffect(() => { - if (curAgentState === AgentState.INIT) { - handleAgentInit(); - } - }, [curAgentState]); - - React.useEffect(() => { - if (statusRef.current === status) { - return; // This is a check because of strict mode - if the status did not change, don't do anything - } - statusRef.current = status; - - if (status !== WsClientProviderStatus.DISCONNECTED && initialQuery) { - dispatch( - addUserMessage({ - content: initialQuery, - imageUrls: files, - timestamp: new Date().toISOString(), - pending: true, - }), - ); - } - - if (status === WsClientProviderStatus.DISCONNECTED) { - dispatch(setCurrentAgentState(AgentState.STOPPED)); - } - }, [status]); -}; diff --git a/frontend/src/routes/_oh.app/route.tsx b/frontend/src/routes/_oh.app/route.tsx index d1bd0c437a0c..fe54e03b4fa9 100644 --- a/frontend/src/routes/_oh.app/route.tsx +++ b/frontend/src/routes/_oh.app/route.tsx @@ -1,7 +1,7 @@ import { useDisclosure } from "@nextui-org/react"; import React from "react"; import { Outlet } from "react-router"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { FaServer } from "react-icons/fa"; import toast from "react-hot-toast"; import { useTranslation } from "react-i18next"; @@ -11,7 +11,7 @@ import { useConversation, } from "#/context/conversation-context"; import { Controls } from "#/components/features/controls/controls"; -import { clearMessages } from "#/state/chat-slice"; +import { clearMessages, addUserMessage } from "#/state/chat-slice"; import { clearTerminal } from "#/state/command-slice"; import { useEffectOnce } from "#/hooks/use-effect-once"; import CodeIcon from "#/icons/code.svg?react"; @@ -36,6 +36,8 @@ import { ServedAppLabel } from "#/components/layout/served-app-label"; import { TerminalStatusLabel } from "#/components/features/terminal/terminal-status-label"; import { useSettings } from "#/hooks/query/use-settings"; import { MULTI_CONVERSATION_UI } from "#/utils/feature-flags"; +import { clearFiles, clearInitialPrompt } from "#/state/initial-query-slice"; +import { RootState } from "#/store"; function AppContent() { useConversationConfig(); @@ -46,6 +48,9 @@ function AppContent() { const { data: conversation, isFetched } = useUserConversation( conversationId || null, ); + const { initialPrompt, files } = useSelector( + (state: RootState) => state.initialQuery, + ); const dispatch = useDispatch(); const endSession = useEndSession(); @@ -74,6 +79,18 @@ function AppContent() { dispatch(clearMessages()); dispatch(clearTerminal()); dispatch(clearJupyter()); + if (conversationId && (initialPrompt || files.length > 0)) { + dispatch( + addUserMessage({ + content: initialPrompt || "", + imageUrls: files || [], + timestamp: new Date().toISOString(), + pending: true, + }), + ); + dispatch(clearInitialPrompt()); + dispatch(clearFiles()); + } }, [conversationId]); useEffectOnce(() => { diff --git a/frontend/src/state/initial-query-slice.ts b/frontend/src/state/initial-query-slice.ts index 73300b50c8d0..1dcbe273ba63 100644 --- a/frontend/src/state/initial-query-slice.ts +++ b/frontend/src/state/initial-query-slice.ts @@ -2,14 +2,14 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; type SliceState = { files: string[]; // base64 encoded images - initialQuery: string | null; + initialPrompt: string | null; selectedRepository: string | null; importedProjectZip: string | null; // base64 encoded zip }; const initialState: SliceState = { files: [], - initialQuery: null, + initialPrompt: null, selectedRepository: null, importedProjectZip: null, }; @@ -27,11 +27,11 @@ export const selectedFilesSlice = createSlice({ clearFiles(state) { state.files = []; }, - setInitialQuery(state, action: PayloadAction) { - state.initialQuery = action.payload; + setInitialPrompt(state, action: PayloadAction) { + state.initialPrompt = action.payload; }, - clearInitialQuery(state) { - state.initialQuery = null; + clearInitialPrompt(state) { + state.initialPrompt = null; }, setSelectedRepository(state, action: PayloadAction) { state.selectedRepository = action.payload; @@ -49,8 +49,8 @@ export const { addFile, removeFile, clearFiles, - setInitialQuery, - clearInitialQuery, + setInitialPrompt, + clearInitialPrompt, setSelectedRepository, clearSelectedRepository, setImportedProjectZip, diff --git a/openhands/agenthub/codeact_agent/function_calling.py b/openhands/agenthub/codeact_agent/function_calling.py index 66f546ec485e..a659f2ccd349 100644 --- a/openhands/agenthub/codeact_agent/function_calling.py +++ b/openhands/agenthub/codeact_agent/function_calling.py @@ -32,6 +32,7 @@ _BASH_DESCRIPTION = """Execute a bash command in the terminal. * Long running commands: For commands that may run indefinitely, it should be run in the background and the output should be redirected to a file, e.g. command = `python3 app.py > server.log 2>&1 &`. * Interact with running process: If a bash command returns exit code `-1`, this means the process is not yet finished. By setting `is_input` to `true`, the assistant can interact with the running process and send empty `command` to retrieve any additional logs, or send additional text (set `command` to the text) to STDIN of the running process, or send command like `C-c` (Ctrl+C), `C-d` (Ctrl+D), `C-z` (Ctrl+Z) to interrupt the process. +* One command at a time: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together. """ CmdRunTool = ChatCompletionToolParam( @@ -44,7 +45,7 @@ 'properties': { 'command': { 'type': 'string', - 'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process.', + 'description': 'The bash command to execute. Can be empty string to view additional logs when previous exit code is `-1`. Can be `C-c` (Ctrl+C) to interrupt the currently running process. Note: You can only execute one bash command at a time. If you need to run multiple commands sequentially, you can use `&&` or `;` to chain them together.', }, 'is_input': { 'type': 'string', diff --git a/openhands/controller/agent_controller.py b/openhands/controller/agent_controller.py index 18f40133036d..d2a9f8b7eb34 100644 --- a/openhands/controller/agent_controller.py +++ b/openhands/controller/agent_controller.py @@ -537,10 +537,6 @@ async def set_agent_state_to(self, new_state: AgentState) -> None: EventSource.ENVIRONMENT, ) - if new_state == AgentState.INIT and self.state.resume_state: - await self.set_agent_state_to(self.state.resume_state) - self.state.resume_state = None - def get_agent_state(self) -> AgentState: """Returns the current state of the agent. diff --git a/openhands/core/schema/action.py b/openhands/core/schema/action.py index 1485f8d7f4a9..3e02357aae1d 100644 --- a/openhands/core/schema/action.py +++ b/openhands/core/schema/action.py @@ -4,10 +4,6 @@ class ActionTypeSchema(BaseModel): - INIT: str = Field(default='initialize') - """Initializes the agent. Only sent by client. - """ - MESSAGE: str = Field(default='message') """Represents a message. """ diff --git a/openhands/core/schema/agent.py b/openhands/core/schema/agent.py index 366bc2c191cc..05fcae5288b9 100644 --- a/openhands/core/schema/agent.py +++ b/openhands/core/schema/agent.py @@ -6,10 +6,6 @@ class AgentState(str, Enum): """The agent is loading. """ - INIT = 'init' - """The agent is initialized. - """ - RUNNING = 'running' """The agent is running. """ diff --git a/openhands/resolver/resolve_issue.py b/openhands/resolver/resolve_issue.py index 2becab9284e4..45c9e33af7c0 100644 --- a/openhands/resolver/resolve_issue.py +++ b/openhands/resolver/resolve_issue.py @@ -567,12 +567,6 @@ def int_or_none(value): choices=['issue', 'pr'], help='Type of issue to resolve, either open issue or pr comments.', ) - parser.add_argument( - '--target-branch', - type=str, - default=None, - help="Target branch to pull and create PR against (for PRs). If not specified, uses the PR's base branch.", - ) parser.add_argument( '--is-experimental', type=lambda x: x.lower() == 'true', diff --git a/openhands/runtime/impl/remote/remote_runtime.py b/openhands/runtime/impl/remote/remote_runtime.py index 79dfd3026d30..f0a9a7fb359d 100644 --- a/openhands/runtime/impl/remote/remote_runtime.py +++ b/openhands/runtime/impl/remote/remote_runtime.py @@ -306,6 +306,12 @@ def _wait_until_alive_impl(self): assert 'pod_status' in runtime_data pod_status = runtime_data['pod_status'].lower() self.log('debug', f'Pod status: {pod_status}') + restart_count = runtime_data.get('restart_count', 0) + if restart_count != 0: + restart_reasons = runtime_data.get('restart_reasons') + self.log( + 'debug', f'Pod restarts: {restart_count}, reasons: {restart_reasons}' + ) # FIXME: We should fix it at the backend of /start endpoint, make sure # the pod is created before returning the response. diff --git a/openhands/server/conversation_manager/conversation_manager.py b/openhands/server/conversation_manager/conversation_manager.py index 21dbffd7fcc7..e96458c5ebdd 100644 --- a/openhands/server/conversation_manager/conversation_manager.py +++ b/openhands/server/conversation_manager/conversation_manager.py @@ -5,6 +5,7 @@ import socketio from openhands.core.config import AppConfig +from openhands.events.action import MessageAction from openhands.events.stream import EventStream from openhands.server.session.conversation import Conversation from openhands.server.settings import Settings @@ -68,7 +69,7 @@ async def maybe_start_agent_loop( sid: str, settings: Settings, user_id: str | None, - initial_user_msg: str | None = None, + initial_user_msg: MessageAction | None = None, ) -> EventStream: """Start an event loop if one is not already running""" diff --git a/openhands/server/conversation_manager/standalone_conversation_manager.py b/openhands/server/conversation_manager/standalone_conversation_manager.py index 9280a8aaea17..078f012a531d 100644 --- a/openhands/server/conversation_manager/standalone_conversation_manager.py +++ b/openhands/server/conversation_manager/standalone_conversation_manager.py @@ -9,6 +9,7 @@ from openhands.core.exceptions import AgentRuntimeUnavailableError from openhands.core.logger import openhands_logger as logger from openhands.core.schema.agent import AgentState +from openhands.events.action import MessageAction from openhands.events.stream import EventStream, session_exists from openhands.server.session.conversation import Conversation from openhands.server.session.session import ROOM_KEY, Session @@ -186,7 +187,7 @@ async def maybe_start_agent_loop( sid: str, settings: Settings, user_id: str | None, - initial_user_msg: str | None = None, + initial_user_msg: MessageAction | None = None, ) -> EventStream: logger.info(f'maybe_start_agent_loop:{sid}') session: Session | None = None diff --git a/openhands/server/listen_socket.py b/openhands/server/listen_socket.py index 5cf56bc2812d..70dcb9998acf 100644 --- a/openhands/server/listen_socket.py +++ b/openhands/server/listen_socket.py @@ -5,7 +5,6 @@ from socketio.exceptions import ConnectionRefusedError from openhands.core.logger import openhands_logger as logger -from openhands.core.schema.agent import AgentState from openhands.events.action import ( NullAction, ) @@ -87,8 +86,6 @@ async def connect(connection_id: str, environ, auth): ): continue elif isinstance(event, AgentStateChangedObservation): - if event.agent_state == AgentState.INIT: - await sio.emit('oh_event', event_to_dict(event), to=connection_id) agent_state_changed = event else: await sio.emit('oh_event', event_to_dict(event), to=connection_id) diff --git a/openhands/server/mock/listen.py b/openhands/server/mock/listen.py index 30aaef68589a..0fcf822a8f9e 100644 --- a/openhands/server/mock/listen.py +++ b/openhands/server/mock/listen.py @@ -2,7 +2,6 @@ from fastapi import FastAPI, WebSocket from openhands.core.logger import openhands_logger as logger -from openhands.core.schema import ActionType from openhands.utils.shutdown_listener import should_continue app = FastAPI() @@ -11,10 +10,6 @@ @app.websocket('/ws') async def websocket_endpoint(websocket: WebSocket): await websocket.accept() - # send message to mock connection - await websocket.send_json( - {'action': ActionType.INIT, 'message': 'Control loop started.'} - ) try: while should_continue(): diff --git a/openhands/server/routes/manage_conversations.py b/openhands/server/routes/manage_conversations.py index 4d3e2e699eff..3fdbbd7d66d8 100644 --- a/openhands/server/routes/manage_conversations.py +++ b/openhands/server/routes/manage_conversations.py @@ -7,6 +7,7 @@ from pydantic import BaseModel from openhands.core.logger import openhands_logger as logger +from openhands.events.action.message import MessageAction from openhands.events.stream import EventStreamSubscriber from openhands.runtime import get_runtime_cls from openhands.server.auth import get_user_id @@ -34,6 +35,7 @@ class InitSessionRequest(BaseModel): github_token: str | None = None selected_repository: str | None = None initial_user_msg: str | None = None + image_urls: list[str] | None = None async def _create_new_conversation( @@ -41,6 +43,7 @@ async def _create_new_conversation( token: str | None, selected_repository: str | None, initial_user_msg: str | None, + image_urls: list[str] | None, ): logger.info('Loading settings') settings_store = await SettingsStoreImpl.get_instance(config, user_id) @@ -94,8 +97,14 @@ async def _create_new_conversation( ) logger.info(f'Starting agent loop for conversation {conversation_id}') + initial_message_action = None + if initial_user_msg or image_urls: + initial_message_action = MessageAction( + content=initial_user_msg or '', + image_urls=image_urls or [], + ) event_stream = await conversation_manager.maybe_start_agent_loop( - conversation_id, conversation_init_data, user_id, initial_user_msg + conversation_id, conversation_init_data, user_id, initial_message_action ) try: event_stream.subscribe( @@ -121,10 +130,16 @@ async def new_conversation(request: Request, data: InitSessionRequest): github_token = getattr(request.state, 'github_token', '') or data.github_token selected_repository = data.selected_repository initial_user_msg = data.initial_user_msg + image_urls = data.image_urls or [] try: + # Create conversation with initial message conversation_id = await _create_new_conversation( - user_id, github_token, selected_repository, initial_user_msg + user_id, + github_token, + selected_repository, + initial_user_msg, + image_urls, ) return JSONResponse( diff --git a/openhands/server/session/README.md b/openhands/server/session/README.md index 12f7e270d622..86cce9194d70 100644 --- a/openhands/server/session/README.md +++ b/openhands/server/session/README.md @@ -8,19 +8,12 @@ interruptions are recoverable. There are 3 main server side event handlers: * `connect` - Invoked when a new connection to the server is established. (This may be via http or WebSocket) -* `oh_action` - Invoked when a connected client sends an event (Such as `INIT` or a prompt for the Agent) - +* `oh_action` - Invoked when a connected client sends an event (such as a prompt for the Agent) - this is distinct from the `oh_event` sent from the server to the client. * `disconnect` - Invoked when a connected client disconnects from the server. -## Init -Each connection has a unique id, and when initially established, is not associated with any session. An -`INIT` event must be sent to the server in order to attach a connection to a session. The `INIT` event -may optionally include a GitHub token and a token to connect to an existing session. (Which may be running -locally or may need to be hydrated). If no token is received as part of the init event, it is assumed a -new session should be started. - ## Disconnect The (manager)[manager.py] manages connections and sessions. Each session may have zero or more connections -associated with it, managed by invocations of `INIT` and disconnect. When a session no longer has any +associated with it. When a session no longer has any connections associated with it, after a set amount of time (determined by `config.sandbox.close_delay`), the session and runtime are passivated (So will need to be rehydrated to continue.) diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index f4c404d4cb5e..c5175178a762 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -10,8 +10,7 @@ from openhands.core.logger import llm_prompt_logger, llm_response_logger from openhands.core.logger import openhands_logger as logger from openhands.core.schema.agent import AgentState -from openhands.events.action import ChangeAgentStateAction -from openhands.events.action.message import MessageAction +from openhands.events.action import ChangeAgentStateAction, MessageAction from openhands.events.event import EventSource from openhands.events.stream import EventStream from openhands.microagent import BaseMicroAgent @@ -73,7 +72,7 @@ async def start( agent_configs: dict[str, AgentConfig] | None = None, github_token: str | None = None, selected_repository: str | None = None, - initial_user_msg: str | None = None, + initial_message: MessageAction | None = None, ): """Starts the Agent session Parameters: @@ -112,15 +111,17 @@ async def start( agent_to_llm_config=agent_to_llm_config, agent_configs=agent_configs, ) - self.event_stream.add_event( - ChangeAgentStateAction(AgentState.INIT), EventSource.ENVIRONMENT - ) - - if initial_user_msg: + if initial_message: + self.event_stream.add_event(initial_message, EventSource.USER) self.event_stream.add_event( - MessageAction(content=initial_user_msg), EventSource.USER + ChangeAgentStateAction(AgentState.RUNNING), EventSource.ENVIRONMENT ) - + else: + self.event_stream.add_event( + ChangeAgentStateAction(AgentState.AWAITING_USER_INPUT), + EventSource.ENVIRONMENT, + ) + self._starting = False async def close(self): diff --git a/openhands/server/session/manager.py b/openhands/server/session/manager.py index 799c56838459..dd4ae4b150c7 100644 --- a/openhands/server/session/manager.py +++ b/openhands/server/session/manager.py @@ -11,6 +11,7 @@ from openhands.core.exceptions import AgentRuntimeUnavailableError from openhands.core.logger import openhands_logger as logger from openhands.core.schema.agent import AgentState +from openhands.events.action import MessageAction from openhands.events.stream import EventStream, session_exists from openhands.server.session.agent_session import WAIT_TIME_BEFORE_CLOSE from openhands.server.session.conversation import Conversation @@ -446,7 +447,7 @@ async def maybe_start_agent_loop( sid: str, settings: Settings, user_id: str | None, - initial_user_msg: str | None = None, + initial_message: MessageAction | None = None, ) -> EventStream: logger.info(f'maybe_start_agent_loop:{sid}') session: Session | None = None @@ -469,7 +470,7 @@ async def maybe_start_agent_loop( user_id=user_id, ) self._local_agent_loops_by_sid[sid] = session - asyncio.create_task(session.initialize_agent(settings, initial_user_msg)) + asyncio.create_task(session.initialize_agent(settings, initial_message)) event_stream = await self._get_event_stream(sid) if not event_stream: diff --git a/openhands/server/session/session.py b/openhands/server/session/session.py index a5b7c58cd933..cf7c75dec74a 100644 --- a/openhands/server/session/session.py +++ b/openhands/server/session/session.py @@ -80,7 +80,9 @@ async def close(self): self.is_alive = False await self.agent_session.close() - async def initialize_agent(self, settings: Settings, initial_user_msg: str | None): + async def initialize_agent( + self, settings: Settings, initial_message: MessageAction | None + ): self.agent_session.event_stream.add_event( AgentStateChangedObservation('', AgentState.LOADING), EventSource.ENVIRONMENT, @@ -134,7 +136,7 @@ async def initialize_agent(self, settings: Settings, initial_user_msg: str | Non agent_configs=self.config.get_agent_configs(), github_token=github_token, selected_repository=selected_repository, - initial_user_msg=initial_user_msg, + initial_message=initial_message, ) except Exception as e: logger.exception(f'Error creating agent_session: {e}') diff --git a/poetry.lock b/poetry.lock index a77061546cec..ea3d47abcf94 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -3944,19 +3944,19 @@ pydantic = ">=1.10" [[package]] name = "llama-index" -version = "0.12.12" +version = "0.12.13" description = "Interface between LLMs and your data" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "llama_index-0.12.12-py3-none-any.whl", hash = "sha256:208f77dba5fd8268cacd3d56ec3ee33b0001d5b6ec623c5b91c755af7b08cfae"}, - {file = "llama_index-0.12.12.tar.gz", hash = "sha256:d4e475726e342b1178736ae3ed93336fe114605e86431b6dfcb454a9e1f26e72"}, + {file = "llama_index-0.12.13-py3-none-any.whl", hash = "sha256:0b285aa451ced6bd8da40df99068ac96badf8b5725c4edc29f2bce4da2ffd8bc"}, + {file = "llama_index-0.12.13.tar.gz", hash = "sha256:1e39a397dcc51dabe280c121fd8d5451a6a84595233a8b26caa54d9b7ecf9ffc"}, ] [package.dependencies] llama-index-agent-openai = ">=0.4.0,<0.5.0" llama-index-cli = ">=0.4.0,<0.5.0" -llama-index-core = ">=0.12.12,<0.13.0" +llama-index-core = ">=0.12.13,<0.13.0" llama-index-embeddings-openai = ">=0.3.0,<0.4.0" llama-index-indices-managed-llama-cloud = ">=0.4.0" llama-index-llms-openai = ">=0.3.0,<0.4.0" @@ -4001,13 +4001,13 @@ llama-index-llms-openai = ">=0.3.0,<0.4.0" [[package]] name = "llama-index-core" -version = "0.12.12" +version = "0.12.13" description = "Interface between LLMs and your data" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "llama_index_core-0.12.12-py3-none-any.whl", hash = "sha256:cea491e87f65e6b775b5aef95720de302b85af1bdc67d779c4b09170a30e5b98"}, - {file = "llama_index_core-0.12.12.tar.gz", hash = "sha256:068b755bbc681731336e822f5977d7608585e8f759c6293ebd812e2659316a37"}, + {file = "llama_index_core-0.12.13-py3-none-any.whl", hash = "sha256:9708bb594bbddffd6ff0767242e49d8978d1ba60a2e62e071d9d123ad2f17e6f"}, + {file = "llama_index_core-0.12.13.tar.gz", hash = "sha256:77af0161246ce1de38efc17cb6438dfff9e9558af00bcfac7dd4d0b7325efa4b"}, ] [package.dependencies] @@ -5469,6 +5469,7 @@ description = "Nvidia JIT LTO Library" optional = false python-versions = ">=3" files = [ + {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_aarch64.whl", hash = "sha256:4abe7fef64914ccfa909bc2ba39739670ecc9e820c83ccc7a6ed414122599b83"}, {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-manylinux2014_x86_64.whl", hash = "sha256:06b3b9b25bf3f8af351d664978ca26a16d2c5127dbd53c0497e28d1fb9611d57"}, {file = "nvidia_nvjitlink_cu12-12.4.127-py3-none-win_amd64.whl", hash = "sha256:fd9020c501d27d135f983c6d3e244b197a7ccad769e34df53a42e276b0e25fa1"}, ] @@ -5581,15 +5582,17 @@ realtime = ["websockets (>=13,<15)"] [[package]] name = "openhands-aci" -version = "0.1.8" +version = "0.1.9" description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." optional = false -python-versions = "^3.12" -files = [] -develop = false +python-versions = "<4.0,>=3.12" +files = [ + {file = "openhands_aci-0.1.9-py3-none-any.whl", hash = "sha256:62af189878db046aa98475a41fa01200efd5ddf1db8a435c38da3d4ad32cb11a"}, + {file = "openhands_aci-0.1.9.tar.gz", hash = "sha256:690d33d355a3e4111f52861dbb96ff766b5a268202324a87c94ba67b628a63b1"}, +] [package.dependencies] -diskcache = "^5.6.3" +diskcache = ">=5.6.3,<6.0.0" flake8 = "*" gitpython = "*" grep-ast = "0.3.3" @@ -5599,13 +5602,7 @@ numpy = "*" pandas = "*" scipy = "*" tree-sitter = "0.21.3" -whatthepatch = "^1.0.6" - -[package.source] -type = "git" -url = "https://github.com/All-Hands-AI/openhands-aci.git" -reference = "fix-find-show-only-hidden-subpaths" -resolved_reference = "910e8c470aff0e496bf262bc673c7ee7b4531159" +whatthepatch = ">=1.0.6,<2.0.0" [[package]] name = "opentelemetry-api" @@ -10134,4 +10131,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "6b74056694bdc84a4583c2f93a5b218f15688827cb59e289eb83331045a1582e" +content-hash = "fbca4b2ca0fe2d1d3cac46164c0c1eb9e468dc6f6bc7165e9a3d62ea9f25d801" diff --git a/pyproject.toml b/pyproject.toml index 8e191f9d53d9..c6fe958ca8b2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ runloop-api-client = "0.13.0" libtmux = ">=0.37,<0.40" pygithub = "^2.5.0" joblib = "*" -openhands-aci = "0.1.8" +openhands-aci = "0.1.9" python-socketio = "^5.11.4" redis = "^5.2.0" sse-starlette = "^2.1.3" @@ -101,6 +101,7 @@ reportlab = "*" [tool.coverage.run] concurrency = ["gevent"] + [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -129,6 +130,7 @@ ignore = ["D1"] [tool.ruff.lint.pydocstyle] convention = "google" + [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*"