Jelajahi Sumber

feat: change Jupyter cwd alone with "bash" (#3331)

* remove unused plugin mixin

* change the entire jupyter PWD with bash;
print jupyter pwd in obs as well;

* remove unused field

* remove unused comments

* change the entire jupyter PWD with bash;
print jupyter pwd in obs as well;

* fix runtime tests for jupyter

* update intgeration tests

* fix test again

---------

Co-authored-by: Graham Neubig <neubig@gmail.com>
Xingyao Wang 1 tahun lalu
induk
melakukan
568e6cdb40

+ 3 - 1
opendevin/runtime/client/client.py

@@ -301,7 +301,7 @@ class RuntimeClient:
                 logger.debug(
                     f"{self.pwd} != {getattr(self, '_jupyter_pwd', None)} -> reset Jupyter PWD"
                 )
-                reset_jupyter_pwd_code = f'import os; os.environ["JUPYTER_PWD"] = os.path.abspath("{self.pwd}")'
+                reset_jupyter_pwd_code = f'import os; os.chdir("{self.pwd}")'
                 _aux_action = IPythonRunCellAction(code=reset_jupyter_pwd_code)
                 _reset_obs = await _jupyter_plugin.run(_aux_action)
                 logger.debug(
@@ -310,6 +310,8 @@ class RuntimeClient:
                 self._jupyter_pwd = self.pwd
 
             obs: IPythonRunCellObservation = await _jupyter_plugin.run(action)
+            obs.content = obs.content.rstrip()
+            obs.content += f'\n[Jupyter current working directory: {self.pwd}]'
             return obs
         else:
             raise RuntimeError(

+ 0 - 65
opendevin/runtime/plugins/agent_skills/agentskills.py

@@ -17,7 +17,6 @@ Functions:
 """
 
 import base64
-import functools
 import os
 import re
 import shutil
@@ -75,52 +74,6 @@ def _get_openai_client():
 # ==================================================================================================
 
 
-# Define the decorator using the functionality of UpdatePwd
-def update_pwd_decorator(func):
-    @functools.wraps(func)
-    def wrapper(*args, **kwargs):
-        jupyter_pwd = os.environ.get('JUPYTER_PWD', None)
-        try:
-            old_pwd = os.getcwd()
-        except FileNotFoundError:
-            import json
-            import subprocess
-
-            print(
-                f'DEBUGGING Environment variables: {json.dumps(dict(os.environ), indent=2)}'
-            )
-            print(f'DEBUGGING User ID: {os.getuid()}, Group ID: {os.getgid()}')
-
-            out = subprocess.run(['pwd'], capture_output=True)
-            old_pwd = out.stdout.decode('utf-8').strip()
-            os.chdir(old_pwd)
-            print(f'DEBUGGING Change to working directory: {old_pwd}')
-
-            import tempfile
-
-            try:
-                tempfile.TemporaryFile(dir=old_pwd)
-                print(f'DEBUGGING Directory {old_pwd} is writable')
-            except Exception as e:
-                print(f'DEBUGGING Directory {old_pwd} is not writable: {str(e)}')
-
-            # ls -alh
-            out = subprocess.run(['ls', '-alh', old_pwd], capture_output=True)
-            print(
-                f'DEBUGGING OLD working directory contents: {out.stdout.decode("utf-8")}'
-            )
-            print(f'DEBUGGING Target JUPYTER pwd: {jupyter_pwd}')
-
-        if jupyter_pwd:
-            os.chdir(jupyter_pwd)
-        try:
-            return func(*args, **kwargs)
-        finally:
-            os.chdir(old_pwd)
-
-    return wrapper
-
-
 def _is_valid_filename(file_name) -> bool:
     if not file_name or not isinstance(file_name, str) or not file_name.strip():
         return False
@@ -240,7 +193,6 @@ def _cur_file_header(current_file, total_lines) -> str:
     return f'[File: {os.path.abspath(current_file)} ({total_lines} lines total)]\n'
 
 
-@update_pwd_decorator
 def open_file(
     path: str, line_number: int | None = 1, context_lines: int | None = WINDOW
 ) -> None:
@@ -277,7 +229,6 @@ def open_file(
     print(output)
 
 
-@update_pwd_decorator
 def goto_line(line_number: int) -> None:
     """Moves the window to show the specified line number.
 
@@ -299,7 +250,6 @@ def goto_line(line_number: int) -> None:
     print(output)
 
 
-@update_pwd_decorator
 def scroll_down() -> None:
     """Moves the window down by 100 lines.
 
@@ -317,7 +267,6 @@ def scroll_down() -> None:
     print(output)
 
 
-@update_pwd_decorator
 def scroll_up() -> None:
     """Moves the window up by 100 lines.
 
@@ -335,7 +284,6 @@ def scroll_up() -> None:
     print(output)
 
 
-@update_pwd_decorator
 def create_file(filename: str) -> None:
     """Creates and opens a new file with the given name.
 
@@ -647,7 +595,6 @@ def _edit_file_impl(
     return ret_str
 
 
-@update_pwd_decorator
 def edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> None:
     """Edit a file. This will search for `to_replace` in the given file and replace it with `new_content`.
 
@@ -749,7 +696,6 @@ def edit_file_by_replace(file_name: str, to_replace: str, new_content: str) -> N
     print(ret_str)
 
 
-@update_pwd_decorator
 def insert_content_at_line(file_name: str, line_number: int, content: str) -> None:
     """Insert content at the given line number in a file.
     This will NOT modify the content of the lines before OR after the given line number.
@@ -784,7 +730,6 @@ def insert_content_at_line(file_name: str, line_number: int, content: str) -> No
     print(ret_str)
 
 
-@update_pwd_decorator
 def append_file(file_name: str, content: str) -> None:
     """Append content to the given file.
     It appends text `content` to the end of the specified file.
@@ -805,7 +750,6 @@ def append_file(file_name: str, content: str) -> None:
     print(ret_str)
 
 
-@update_pwd_decorator
 def search_dir(search_term: str, dir_path: str = './') -> None:
     """Searches for search_term in all files in dir. If dir is not provided, searches in the current directory.
 
@@ -845,7 +789,6 @@ def search_dir(search_term: str, dir_path: str = './') -> None:
     print(f'[End of matches for "{search_term}" in {dir_path}]')
 
 
-@update_pwd_decorator
 def search_file(search_term: str, file_path: Optional[str] = None) -> None:
     """Searches for search_term in file. If file is not provided, searches in the current open file.
 
@@ -878,7 +821,6 @@ def search_file(search_term: str, file_path: Optional[str] = None) -> None:
         print(f'[No matches found for "{search_term}" in {file_path}]')
 
 
-@update_pwd_decorator
 def find_file(file_name: str, dir_path: str = './') -> None:
     """Finds all files with the given name in the specified directory.
 
@@ -904,7 +846,6 @@ def find_file(file_name: str, dir_path: str = './') -> None:
         print(f'[No matches found for "{file_name}" in {dir_path}]')
 
 
-@update_pwd_decorator
 def parse_pdf(file_path: str) -> None:
     """Parses the content of a PDF file and prints it.
 
@@ -923,7 +864,6 @@ def parse_pdf(file_path: str) -> None:
     print(text.strip())
 
 
-@update_pwd_decorator
 def parse_docx(file_path: str) -> None:
     """Parses the content of a DOCX file and prints it.
 
@@ -938,7 +878,6 @@ def parse_docx(file_path: str) -> None:
     print(text)
 
 
-@update_pwd_decorator
 def parse_latex(file_path: str) -> None:
     """Parses the content of a LaTex file and prints it.
 
@@ -991,7 +930,6 @@ def _prepare_image_messages(task: str, base64_image: str):
     ]
 
 
-@update_pwd_decorator
 def parse_audio(file_path: str, model: str = 'whisper-1') -> None:
     """Parses the content of an audio file and prints it.
 
@@ -1012,7 +950,6 @@ def parse_audio(file_path: str, model: str = 'whisper-1') -> None:
         print(f'Error transcribing audio file: {e}')
 
 
-@update_pwd_decorator
 def parse_image(
     file_path: str, task: str = 'Describe this image as detail as possible.'
 ) -> None:
@@ -1038,7 +975,6 @@ def parse_image(
         print(f'Error with the request: {error}')
 
 
-@update_pwd_decorator
 def parse_video(
     file_path: str,
     task: str = 'Describe this image as detail as possible.',
@@ -1086,7 +1022,6 @@ def parse_video(
             print(f'Error with the request: {error}')
 
 
-@update_pwd_decorator
 def parse_pptx(file_path: str) -> None:
     """Parses the content of a pptx file and prints it.
 

+ 2 - 0
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_001.log

@@ -396,6 +396,8 @@ The server is running on port 5000 with PID 126. You can access the list of numb
 
 NOW, LET'S START!
 
+----------
+
 Use Jupyter IPython to write a text file containing 'hello world' to '/workspace/test.txt'. Do not ask me for confirmation at any point.
 
 ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with <finish></finish>.

+ 2 - 1
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython/prompt_002.log

@@ -412,5 +412,6 @@ with open('/workspace/test.txt', 'w') as file:
 
 OBSERVATION:
 [Code executed successfully with no output]
+[Jupyter current working directory: /workspace]
 
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>.

+ 2 - 0
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_001.log

@@ -396,6 +396,8 @@ The server is running on port 5000 with PID 126. You can access the list of numb
 
 NOW, LET'S START!
 
+----------
+
 Install and import pymsgbox==1.0.9 and print it's version in /workspace/test.txt. Do not ask me for confirmation at any point.
 
 ENVIRONMENT REMINDER: You have 14 turns left to complete the task. When finished reply with <finish></finish>.

+ 4 - 4
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_002.log

@@ -412,12 +412,12 @@ Sure! Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=0aa91e03ed30fdcf42db3522fd0444c80951cccb215529a6ed9a95fc2b8a6f0c
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -426,6 +426,6 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
+[Jupyter current working directory: /workspace]
 
-
-ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>
+ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>.

+ 5 - 4
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_003.log

@@ -412,12 +412,12 @@ Sure! Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=0aa91e03ed30fdcf42db3522fd0444c80951cccb215529a6ed9a95fc2b8a6f0c
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -426,7 +426,7 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
-
+[Jupyter current working directory: /workspace]
 
 ----------
 
@@ -444,5 +444,6 @@ with open('/workspace/test.txt', 'w') as file:
 
 OBSERVATION:
 [Code executed successfully with no output]
+[Jupyter current working directory: /workspace]
 
-ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with <finish></finish>
+ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with <finish></finish>.

+ 5 - 4
tests/integration/mock/eventstream_runtime/CodeActAgent/test_ipython_module/prompt_004.log

@@ -412,12 +412,12 @@ Sure! Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=7cc4ff18827e3c2e2bd0780a92ca70fd625dc7645b994b6f3191dfb0660e606e
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=0aa91e03ed30fdcf42db3522fd0444c80951cccb215529a6ed9a95fc2b8a6f0c
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -426,7 +426,7 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
-
+[Jupyter current working directory: /workspace]
 
 ----------
 
@@ -444,6 +444,7 @@ with open('/workspace/test.txt', 'w') as file:
 
 OBSERVATION:
 [Code executed successfully with no output]
+[Jupyter current working directory: /workspace]
 
 ----------
 
@@ -460,4 +461,4 @@ pymsgbox version: 1.0.9
 opendevin@docker-desktop:/workspace $
 [Command -1 finished with exit code 0]
 
-ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with <finish></finish>
+ENVIRONMENT REMINDER: You have 11 turns left to complete the task. When finished reply with <finish></finish>.

+ 1 - 0
tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython/prompt_002.log

@@ -603,5 +603,6 @@ with open('/workspace/test.txt', 'w') as file:
 
 OBSERVATION:
 [Code executed successfully with no output]
+[Jupyter current working directory: /workspace]
 
 ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>.

+ 3 - 3
tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_002.log

@@ -603,12 +603,12 @@ Understood. Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / - \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - \ done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e6c344aecd9e7b02d3ff2bb4d98a74d0fe6156b5f523d40b402350da7aac55e6
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -617,6 +617,6 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
-
+[Jupyter current working directory: /workspace]
 
 ENVIRONMENT REMINDER: You have 13 turns left to complete the task. When finished reply with <finish></finish>.

+ 4 - 3
tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_003.log

@@ -603,12 +603,12 @@ Understood. Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / - \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - \ done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e6c344aecd9e7b02d3ff2bb4d98a74d0fe6156b5f523d40b402350da7aac55e6
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -617,7 +617,7 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
-
+[Jupyter current working directory: /workspace]
 
 ----------
 
@@ -637,5 +637,6 @@ version_info
 
 OBSERVATION:
 '1.0.9'
+[Jupyter current working directory: /workspace]
 
 ENVIRONMENT REMINDER: You have 12 turns left to complete the task. When finished reply with <finish></finish>.

+ 4 - 3
tests/integration/mock/eventstream_runtime/CodeActSWEAgent/test_ipython_module/prompt_004.log

@@ -603,12 +603,12 @@ Understood. Let's start by installing the `pymsgbox` package.
 OBSERVATION:
 Collecting pymsgbox==1.0.9
   Downloading PyMsgBox-1.0.9.tar.gz (18 kB)
-  Installing build dependencies ... [?25l- \ | / - \ | / done
+  Installing build dependencies ... [?25l- \ | / - \ | / - \ | / - \ | / - \ done
 [?25h  Getting requirements to build wheel ... [?25l- done
 [?25h  Preparing metadata (pyproject.toml) ... [?25l- done
 [?25hBuilding wheels for collected packages: pymsgbox
   Building wheel for pymsgbox (pyproject.toml) ... [?25l- done
-[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e504d1b40c05b7da59bba1f908d23edcd98381d8e0ecc41a1162745ee4ee6fd2
+[?25h  Created wheel for pymsgbox: filename=PyMsgBox-1.0.9-py3-none-any.whl size=7406 sha256=e6c344aecd9e7b02d3ff2bb4d98a74d0fe6156b5f523d40b402350da7aac55e6
   Stored in directory: /home/opendevin/.cache/pip/wheels/85/92/63/e126ee5f33d8f2ed04f96e43ef5df7270a2f331848752e8662
 Successfully built pymsgbox
 Installing collected packages: pymsgbox
@@ -617,7 +617,7 @@ Successfully installed pymsgbox-1.0.9
 [notice] A new release of pip is available: 24.1 -> 24.2
 [notice] To update, run: pip install --upgrade pip
 Note: you may need to restart the kernel to use updated packages.
-
+[Jupyter current working directory: /workspace]
 
 ----------
 
@@ -637,6 +637,7 @@ version_info
 
 OBSERVATION:
 '1.0.9'
+[Jupyter current working directory: /workspace]
 
 ----------
 

+ 23 - 5
tests/unit/test_runtime.py

@@ -303,7 +303,10 @@ async def test_simple_cmd_ipython_and_fileop(temp_dir, box_class, run_as_devin):
     assert isinstance(obs, IPythonRunCellObservation)
 
     logger.info(obs, extra={'msg_type': 'OBSERVATION'})
-    assert obs.content.strip() == 'Hello, `World`!'
+    assert (
+        obs.content.strip()
+        == 'Hello, `World`!\n[Jupyter current working directory: /workspace]'
+    )
 
     # Test read file (file should not exist)
     action_read = FileReadAction(path='hello.sh')
@@ -768,7 +771,10 @@ async def test_ipython_multi_user(temp_dir, box_class, run_as_devin):
     obs = await runtime.run_action(action_ipython)
     assert isinstance(obs, IPythonRunCellObservation)
     logger.info(obs, extra={'msg_type': 'OBSERVATION'})
-    assert obs.content.strip() == '/workspace'
+    assert (
+        obs.content.strip()
+        == '/workspace\n[Jupyter current working directory: /workspace]'
+    )
 
     # write a file
     test_code = "with open('test.txt', 'w') as f: f.write('Hello, world!')"
@@ -777,7 +783,10 @@ async def test_ipython_multi_user(temp_dir, box_class, run_as_devin):
     obs = await runtime.run_action(action_ipython)
     logger.info(obs, extra={'msg_type': 'OBSERVATION'})
     assert isinstance(obs, IPythonRunCellObservation)
-    assert obs.content.strip() == '[Code executed successfully with no output]'
+    assert (
+        obs.content.strip()
+        == '[Code executed successfully with no output]\n[Jupyter current working directory: /workspace]'
+    )
 
     # check file owner via bash
     action = CmdRunAction(command='ls -alh test.txt')
@@ -816,7 +825,7 @@ async def test_ipython_simple(temp_dir, box_class):
     obs = await runtime.run_action(action_ipython)
     assert isinstance(obs, IPythonRunCellObservation)
     logger.info(obs, extra={'msg_type': 'OBSERVATION'})
-    assert obs.content.strip() == '1'
+    assert obs.content.strip() == '1\n[Jupyter current working directory: /workspace]'
 
     await runtime.close()
     await asyncio.sleep(1)
@@ -850,6 +859,7 @@ async def _test_ipython_agentskills_fileop_pwd_impl(
         '1|\n'
         '(this is the end of the file)\n'
         '[File hello.py created.]\n'
+        '[Jupyter current working directory: /workspace]'
     ).strip().split('\n')
 
     action = CmdRunAction(command='cd test')
@@ -872,6 +882,7 @@ async def _test_ipython_agentskills_fileop_pwd_impl(
         '1|\n'
         '(this is the end of the file)\n'
         '[File hello.py created.]\n'
+        '[Jupyter current working directory: /workspace/test]'
     ).strip().split('\n')
 
     if enable_auto_lint:
@@ -904,6 +915,7 @@ ERRORS:
 Your changes have NOT been applied. Please fix your edit command and try again.
 You either need to 1) Specify the correct start/end line arguments or 2) Correct your edit code.
 DO NOT re-run the same failed edit command. Running it again will lead to the same error.
+[Jupyter current working directory: /workspace/test]
 """
         ).strip().split('\n')
 
@@ -922,6 +934,7 @@ DO NOT re-run the same failed edit command. Running it again will lead to the sa
 1|print("hello world")
 (this is the end of the file)
 [File updated (edited at line 1). Please review the changes and make sure they are correct (correct indentation, no duplicate lines, etc). Edit the file again if necessary.]
+[Jupyter current working directory: /workspace/test]
 """
     ).strip().split('\n')
 
@@ -988,6 +1001,7 @@ async def test_ipython_agentskills_fileop_pwd_with_userdir(temp_dir, box_class):
         '1|\n'
         '(this is the end of the file)\n'
         '[File hello.py created.]\n'
+        '[Jupyter current working directory: /root]'
     ).strip().split('\n')
 
     action = CmdRunAction(command='cd test')
@@ -1010,6 +1024,7 @@ async def test_ipython_agentskills_fileop_pwd_with_userdir(temp_dir, box_class):
         '1|\n'
         '(this is the end of the file)\n'
         '[File hello.py created.]\n'
+        '[Jupyter current working directory: /root/test]'
     ).strip().split('\n')
 
     await runtime.close()
@@ -1073,7 +1088,10 @@ async def test_ipython_package_install(temp_dir, box_class, run_as_devin):
     obs = await runtime.run_action(action)
     logger.info(obs, extra={'msg_type': 'OBSERVATION'})
     # import should not error out
-    assert obs.content.strip() == '[Code executed successfully with no output]'
+    assert (
+        obs.content.strip()
+        == '[Code executed successfully with no output]\n[Jupyter current working directory: /workspace]'
+    )
 
     await runtime.close()
     await asyncio.sleep(1)