From b057af8d63a3e434238844fec91aed3fb416120f Mon Sep 17 00:00:00 2001 From: Tim O'Farrell Date: Wed, 16 Jul 2025 15:10:03 -0600 Subject: [PATCH] Feat: Add current working directory to LLM instructions (#9718) --- .../agenthub/codeact_agent/prompts/additional_info.j2 | 5 ++++- .../agenthub/readonly_agent/prompts/additional_info.j2 | 3 +++ openhands/cli/main.py | 1 + openhands/core/config/config_utils.py | 1 + openhands/core/config/openhands_config.py | 8 ++++++-- openhands/core/main.py | 1 + openhands/core/setup.py | 4 +++- openhands/events/observation/agent.py | 1 + openhands/memory/conversation_memory.py | 2 ++ openhands/memory/memory.py | 4 ++++ openhands/server/session/agent_session.py | 6 +++++- openhands/utils/prompt.py | 1 + tests/unit/test_memory.py | 3 ++- tests/unit/test_observation_serialization.py | 2 ++ tests/unit/test_prompt_manager.py | 1 + 15 files changed, 37 insertions(+), 6 deletions(-) diff --git a/openhands/agenthub/codeact_agent/prompts/additional_info.j2 b/openhands/agenthub/codeact_agent/prompts/additional_info.j2 index 3d08e0e657..50315d11ec 100644 --- a/openhands/agenthub/codeact_agent/prompts/additional_info.j2 +++ b/openhands/agenthub/codeact_agent/prompts/additional_info.j2 @@ -1,6 +1,6 @@ {% if repository_info %} -At the user's request, repository {{ repository_info.repo_name }} has been cloned to the current working directory {{ repository_info.repo_directory }}. +At the user's request, repository {{ repository_info.repo_name }} has been cloned to {{ repository_info.repo_directory }} in the current working directory. {% endif %} {% if repository_instructions -%} @@ -10,6 +10,9 @@ At the user's request, repository {{ repository_info.repo_name }} has been clone {% endif %} {% if runtime_info -%} +{% if runtime_info.working_dir %} +The current working directory is {{ runtime_info.working_dir }} +{% endif %} {% if runtime_info.available_hosts %} The user has access to the following hosts for accessing a web application, each of which has a corresponding port: diff --git a/openhands/agenthub/readonly_agent/prompts/additional_info.j2 b/openhands/agenthub/readonly_agent/prompts/additional_info.j2 index f3b50b0a37..12d6976665 100644 --- a/openhands/agenthub/readonly_agent/prompts/additional_info.j2 +++ b/openhands/agenthub/readonly_agent/prompts/additional_info.j2 @@ -10,6 +10,9 @@ At the user's request, repository {{ repository_info.repo_name }} has been clone {% endif %} {% if runtime_info and (runtime_info.additional_agent_instructions or runtime_info.date) -%} +{% if runtime_info.working_dir %} +The current working directory is {{ runtime_info.working_dir }} +{% endif %} {% if runtime_info.additional_agent_instructions %} {{ runtime_info.additional_agent_instructions }} {% endif %} diff --git a/openhands/cli/main.py b/openhands/cli/main.py index 43a0e39cf1..fba4178ddc 100644 --- a/openhands/cli/main.py +++ b/openhands/cli/main.py @@ -277,6 +277,7 @@ async def run_session( selected_repository=config.sandbox.selected_repo, repo_directory=repo_directory, conversation_instructions=conversation_instructions, + working_dir=os.getcwd(), ) # Add MCP tools to the agent diff --git a/openhands/core/config/config_utils.py b/openhands/core/config/config_utils.py index e8eb810672..5dd5c5a98c 100644 --- a/openhands/core/config/config_utils.py +++ b/openhands/core/config/config_utils.py @@ -6,6 +6,7 @@ from pydantic.fields import FieldInfo OH_DEFAULT_AGENT = 'CodeActAgent' OH_MAX_ITERATIONS = 500 +DEFAULT_WORKSPACE_MOUNT_PATH_IN_SANDBOX = '/workspace' def get_field_info(field: FieldInfo) -> dict[str, Any]: diff --git a/openhands/core/config/openhands_config.py b/openhands/core/config/openhands_config.py index 16f931400a..790c990d96 100644 --- a/openhands/core/config/openhands_config.py +++ b/openhands/core/config/openhands_config.py @@ -7,6 +7,7 @@ from openhands.core import logger from openhands.core.config.agent_config import AgentConfig from openhands.core.config.cli_config import CLIConfig from openhands.core.config.config_utils import ( + DEFAULT_WORKSPACE_MOUNT_PATH_IN_SANDBOX, OH_DEFAULT_AGENT, OH_MAX_ITERATIONS, model_defaults_to_dict, @@ -78,10 +79,13 @@ class OpenHandsConfig(BaseModel): description='API key for Tavily search engine (https://tavily.com/). Required for search functionality.', ) + workspace_base: str | None = Field(default=None) + workspace_mount_path_in_sandbox: str = Field( + default=DEFAULT_WORKSPACE_MOUNT_PATH_IN_SANDBOX + ) + # Deprecated parameters - will be removed in a future version - workspace_base: str | None = Field(default=None, deprecated=True) workspace_mount_path: str | None = Field(default=None, deprecated=True) - workspace_mount_path_in_sandbox: str = Field(default='/workspace', deprecated=True) workspace_mount_rewrite: str | None = Field(default=None, deprecated=True) # End of deprecated parameters diff --git a/openhands/core/main.py b/openhands/core/main.py index d6f0d67c0e..3979d6941d 100644 --- a/openhands/core/main.py +++ b/openhands/core/main.py @@ -130,6 +130,7 @@ async def run_controller( selected_repository=config.sandbox.selected_repo, repo_directory=repo_directory, conversation_instructions=conversation_instructions, + working_dir=config.workspace_mount_path_in_sandbox, ) # Add MCP tools to the agent diff --git a/openhands/core/setup.py b/openhands/core/setup.py index 20142fed3a..2ef674b122 100644 --- a/openhands/core/setup.py +++ b/openhands/core/setup.py @@ -12,6 +12,7 @@ from openhands.controller.state.state import State from openhands.core.config import ( OpenHandsConfig, ) +from openhands.core.config.config_utils import DEFAULT_WORKSPACE_MOUNT_PATH_IN_SANDBOX from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream from openhands.events.event import Event @@ -140,6 +141,7 @@ def create_memory( repo_directory: str | None = None, status_callback: Callable | None = None, conversation_instructions: str | None = None, + working_dir: str = DEFAULT_WORKSPACE_MOUNT_PATH_IN_SANDBOX, ) -> Memory: """Create a memory for the agent to use. @@ -162,7 +164,7 @@ def create_memory( if runtime: # sets available hosts - memory.set_runtime_info(runtime, {}) + memory.set_runtime_info(runtime, {}, working_dir) # loads microagents from repo/.openhands/microagents microagents: list[BaseMicroagent] = runtime.get_microagents_from_selected_repo( diff --git a/openhands/events/observation/agent.py b/openhands/events/observation/agent.py index 8884e5c1e7..d7252f334a 100644 --- a/openhands/events/observation/agent.py +++ b/openhands/events/observation/agent.py @@ -76,6 +76,7 @@ class RecallObservation(Observation): date: str = '' custom_secrets_descriptions: dict[str, str] = field(default_factory=dict) conversation_instructions: str = '' + working_dir: str = '' # knowledge microagent_knowledge: list[MicroagentKnowledge] = field(default_factory=list) diff --git a/openhands/memory/conversation_memory.py b/openhands/memory/conversation_memory.py index ba4c4b0319..5933429f22 100644 --- a/openhands/memory/conversation_memory.py +++ b/openhands/memory/conversation_memory.py @@ -524,11 +524,13 @@ class ConversationMemory: additional_agent_instructions=obs.additional_agent_instructions, date=date, custom_secrets_descriptions=obs.custom_secrets_descriptions, + working_dir=obs.working_dir, ) else: runtime_info = RuntimeInfo( date=date, custom_secrets_descriptions=obs.custom_secrets_descriptions, + working_dir=obs.working_dir, ) conversation_instructions = None diff --git a/openhands/memory/memory.py b/openhands/memory/memory.py index 5a1eb82e36..1e75ded246 100644 --- a/openhands/memory/memory.py +++ b/openhands/memory/memory.py @@ -198,6 +198,7 @@ class Memory: conversation_instructions=self.conversation_instructions.content if self.conversation_instructions is not None else '', + working_dir=self.runtime_info.working_dir if self.runtime_info else '', ) return obs return None @@ -332,6 +333,7 @@ class Memory: self, runtime: Runtime, custom_secrets_descriptions: dict[str, str], + working_dir: str, ) -> None: """Store runtime info (web hosts, ports, etc.).""" # e.g. { '127.0.0.1': 8080 } @@ -344,11 +346,13 @@ class Memory: additional_agent_instructions=runtime.additional_agent_instructions, date=date, custom_secrets_descriptions=custom_secrets_descriptions, + working_dir=working_dir, ) else: self.runtime_info = RuntimeInfo( date=date, custom_secrets_descriptions=custom_secrets_descriptions, + working_dir=working_dir, ) def set_conversation_instructions( diff --git a/openhands/server/session/agent_session.py b/openhands/server/session/agent_session.py index 8dff8a2924..af4496a3f2 100644 --- a/openhands/server/session/agent_session.py +++ b/openhands/server/session/agent_session.py @@ -154,6 +154,7 @@ class AgentSession: repo_directory=repo_directory, conversation_instructions=conversation_instructions, custom_secrets_descriptions=custom_secrets_handler.get_custom_secrets_descriptions(), + working_dir=config.workspace_mount_path_in_sandbox, ) # NOTE: this needs to happen before controller is created @@ -464,6 +465,7 @@ class AgentSession: repo_directory: str | None, conversation_instructions: str | None, custom_secrets_descriptions: dict[str, str], + working_dir: str, ) -> Memory: memory = Memory( event_stream=self.event_stream, @@ -473,7 +475,9 @@ class AgentSession: if self.runtime: # sets available hosts and other runtime info - memory.set_runtime_info(self.runtime, custom_secrets_descriptions) + memory.set_runtime_info( + self.runtime, custom_secrets_descriptions, working_dir + ) memory.set_conversation_instructions(conversation_instructions) # loads microagents from repo/.openhands/microagents diff --git a/openhands/utils/prompt.py b/openhands/utils/prompt.py index a694f31321..467e8eb160 100644 --- a/openhands/utils/prompt.py +++ b/openhands/utils/prompt.py @@ -15,6 +15,7 @@ class RuntimeInfo: available_hosts: dict[str, int] = field(default_factory=dict) additional_agent_instructions: str = '' custom_secrets_descriptions: dict[str, str] = field(default_factory=dict) + working_dir: str = '' @dataclass diff --git a/tests/unit/test_memory.py b/tests/unit/test_memory.py index a11536c10a..19b2eb96d8 100644 --- a/tests/unit/test_memory.py +++ b/tests/unit/test_memory.py @@ -389,7 +389,7 @@ async def test_custom_secrets_descriptions(): } # Set runtime info with custom secrets - memory.set_runtime_info(mock_runtime, custom_secrets) + memory.set_runtime_info(mock_runtime, custom_secrets, '/workspace') # Set repository info memory.set_repository_info('test-owner/test-repo', '/workspace/test-repo') @@ -448,6 +448,7 @@ def test_custom_secrets_descriptions_serialization(prompt_dir): available_hosts={'test-host.example.com': 8080}, additional_agent_instructions='Test instructions', custom_secrets_descriptions=custom_secrets, + working_dir='/workspace', ) # Create a RepositoryInfo diff --git a/tests/unit/test_observation_serialization.py b/tests/unit/test_observation_serialization.py index 74c91e5cc9..138151ef3d 100644 --- a/tests/unit/test_observation_serialization.py +++ b/tests/unit/test_observation_serialization.py @@ -242,6 +242,7 @@ def test_microagent_observation_serialization(): 'recall_type': 'workspace_context', 'repo_name': 'some_repo_name', 'repo_directory': 'some_repo_directory', + 'working_dir': '', 'runtime_hosts': {'host1': 8080, 'host2': 8081}, 'repo_instructions': 'complex_repo_instructions', 'additional_agent_instructions': 'You know it all about this runtime', @@ -265,6 +266,7 @@ def test_microagent_observation_microagent_knowledge_serialization(): 'repo_directory': '', 'repo_instructions': '', 'runtime_hosts': {}, + 'working_dir': '', 'additional_agent_instructions': '', 'custom_secrets_descriptions': {}, 'conversation_instructions': 'additional_context', diff --git a/tests/unit/test_prompt_manager.py b/tests/unit/test_prompt_manager.py index f4b8b5eec3..45d0894a42 100644 --- a/tests/unit/test_prompt_manager.py +++ b/tests/unit/test_prompt_manager.py @@ -236,6 +236,7 @@ Today's date is {{ runtime_info.date }} date='02/12/1232', available_hosts={'example.com': 8080}, additional_agent_instructions='You know everything about this runtime.', + working_dir='/workspace', ) repo_instructions = 'This repository contains important code.'