test_is_stuck.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. import logging
  2. from unittest.mock import Mock, patch
  3. import pytest
  4. from pytest import TempPathFactory
  5. from openhands.controller.agent_controller import AgentController
  6. from openhands.controller.state.state import State
  7. from openhands.controller.stuck import StuckDetector
  8. from openhands.events.action import CmdRunAction, FileReadAction, MessageAction
  9. from openhands.events.action.commands import IPythonRunCellAction
  10. from openhands.events.observation import (
  11. CmdOutputObservation,
  12. FileReadObservation,
  13. )
  14. from openhands.events.observation.commands import IPythonRunCellObservation
  15. from openhands.events.observation.empty import NullObservation
  16. from openhands.events.observation.error import ErrorObservation
  17. from openhands.events.stream import EventSource, EventStream
  18. from openhands.storage import get_file_store
  19. def collect_events(stream):
  20. return [event for event in stream.get_events()]
  21. logging.basicConfig(level=logging.DEBUG)
  22. jupyter_line_1 = '\n[Jupyter current working directory:'
  23. jupyter_line_2 = '\n[Jupyter Python interpreter:'
  24. code_snippet = """
  25. edit_file_by_replace(
  26. 'book_store.py',
  27. to_replace=\"""def total(basket):
  28. if not basket:
  29. return 0
  30. """
  31. @pytest.fixture
  32. def temp_dir(tmp_path_factory: TempPathFactory) -> str:
  33. return str(tmp_path_factory.mktemp('test_is_stuck'))
  34. @pytest.fixture
  35. def event_stream(temp_dir):
  36. file_store = get_file_store('local', temp_dir)
  37. event_stream = EventStream('asdf', file_store)
  38. yield event_stream
  39. # clear after each test
  40. event_stream.clear()
  41. class TestStuckDetector:
  42. @pytest.fixture
  43. def stuck_detector(self):
  44. state = State(inputs={}, max_iterations=50)
  45. state.history = [] # Initialize history as an empty list
  46. return StuckDetector(state)
  47. def _impl_syntax_error_events(
  48. self,
  49. state: State,
  50. error_message: str,
  51. random_line: bool,
  52. incidents: int = 4,
  53. ):
  54. for i in range(incidents):
  55. ipython_action = IPythonRunCellAction(code=code_snippet)
  56. state.history.append(ipython_action)
  57. extra_number = (i + 1) * 10 if random_line else '42'
  58. extra_line = '\n' * (i + 1) if random_line else ''
  59. ipython_observation = IPythonRunCellObservation(
  60. content=f' Cell In[1], line {extra_number}\n'
  61. 'to_replace="""def largest(min_factor, max_factor):\n ^\n'
  62. f'{error_message}{extra_line}' + jupyter_line_1 + jupyter_line_2,
  63. code=code_snippet,
  64. )
  65. # ipython_observation._cause = ipython_action._id
  66. state.history.append(ipython_observation)
  67. def _impl_unterminated_string_error_events(
  68. self, state: State, random_line: bool, incidents: int = 4
  69. ):
  70. for i in range(incidents):
  71. ipython_action = IPythonRunCellAction(code=code_snippet)
  72. state.history.append(ipython_action)
  73. line_number = (i + 1) * 10 if random_line else '1'
  74. ipython_observation = IPythonRunCellObservation(
  75. content=f'print(" Cell In[1], line {line_number}\nhello\n ^\nSyntaxError: unterminated string literal (detected at line {line_number})'
  76. + jupyter_line_1
  77. + jupyter_line_2,
  78. code=code_snippet,
  79. )
  80. # ipython_observation._cause = ipython_action._
  81. state.history.append(ipython_observation)
  82. def test_history_too_short(self, stuck_detector: StuckDetector):
  83. state = stuck_detector.state
  84. message_action = MessageAction(content='Hello', wait_for_response=False)
  85. message_action._source = EventSource.USER
  86. observation = NullObservation(content='')
  87. # observation._cause = message_action.id
  88. state.history.append(message_action)
  89. state.history.append(observation)
  90. cmd_action = CmdRunAction(command='ls')
  91. state.history.append(cmd_action)
  92. cmd_observation = CmdOutputObservation(
  93. command_id=1, command='ls', content='file1.txt\nfile2.txt'
  94. )
  95. # cmd_observation._cause = cmd_action._id
  96. state.history.append(cmd_observation)
  97. assert stuck_detector.is_stuck(headless_mode=True) is False
  98. def test_interactive_mode_resets_after_user_message(
  99. self, stuck_detector: StuckDetector
  100. ):
  101. state = stuck_detector.state
  102. # First add some actions that would be stuck in non-UI mode
  103. for i in range(4):
  104. cmd_action = CmdRunAction(command='ls')
  105. cmd_action._id = i
  106. state.history.append(cmd_action)
  107. cmd_observation = CmdOutputObservation(
  108. content='', command='ls', command_id=i
  109. )
  110. cmd_observation._cause = cmd_action._id
  111. state.history.append(cmd_observation)
  112. # In headless mode, this should be stuck
  113. assert stuck_detector.is_stuck(headless_mode=True) is True
  114. # with the UI, it will ALSO be stuck initially
  115. assert stuck_detector.is_stuck(headless_mode=False) is True
  116. # Add a user message
  117. message_action = MessageAction(content='Hello', wait_for_response=False)
  118. message_action._source = EventSource.USER
  119. state.history.append(message_action)
  120. # In not-headless mode, this should not be stuck because we ignore history before user message
  121. assert stuck_detector.is_stuck(headless_mode=False) is False
  122. # But in headless mode, this should be still stuck because user messages do not count
  123. assert stuck_detector.is_stuck(headless_mode=True) is True
  124. # Add two more identical actions - still not stuck because we need at least 3
  125. for i in range(2):
  126. cmd_action = CmdRunAction(command='ls')
  127. cmd_action._id = i + 4
  128. state.history.append(cmd_action)
  129. cmd_observation = CmdOutputObservation(
  130. content='', command='ls', command_id=i + 4
  131. )
  132. cmd_observation._cause = cmd_action._id
  133. state.history.append(cmd_observation)
  134. assert stuck_detector.is_stuck(headless_mode=False) is False
  135. # Add two more identical actions - now it should be stuck
  136. for i in range(2):
  137. cmd_action = CmdRunAction(command='ls')
  138. cmd_action._id = i + 6
  139. state.history.append(cmd_action)
  140. cmd_observation = CmdOutputObservation(
  141. content='', command='ls', command_id=i + 6
  142. )
  143. cmd_observation._cause = cmd_action._id
  144. state.history.append(cmd_observation)
  145. assert stuck_detector.is_stuck(headless_mode=False) is True
  146. def test_is_stuck_repeating_action_observation(self, stuck_detector: StuckDetector):
  147. state = stuck_detector.state
  148. message_action = MessageAction(content='Done', wait_for_response=False)
  149. message_action._source = EventSource.USER
  150. hello_action = MessageAction(content='Hello', wait_for_response=False)
  151. hello_observation = NullObservation('')
  152. # 2 events
  153. state.history.append(hello_action)
  154. state.history.append(hello_observation)
  155. cmd_action_1 = CmdRunAction(command='ls')
  156. cmd_action_1._id = 1
  157. state.history.append(cmd_action_1)
  158. cmd_observation_1 = CmdOutputObservation(content='', command='ls', command_id=1)
  159. cmd_observation_1._cause = cmd_action_1._id
  160. state.history.append(cmd_observation_1)
  161. # 4 events
  162. cmd_action_2 = CmdRunAction(command='ls')
  163. cmd_action_2._id = 2
  164. state.history.append(cmd_action_2)
  165. cmd_observation_2 = CmdOutputObservation(content='', command='ls', command_id=2)
  166. cmd_observation_2._cause = cmd_action_2._id
  167. state.history.append(cmd_observation_2)
  168. # 6 events
  169. # random user message just because we can
  170. message_null_observation = NullObservation(content='')
  171. state.history.append(message_action)
  172. state.history.append(message_null_observation)
  173. # 8 events
  174. assert stuck_detector.is_stuck(headless_mode=True) is False
  175. cmd_action_3 = CmdRunAction(command='ls')
  176. cmd_action_3._id = 3
  177. state.history.append(cmd_action_3)
  178. cmd_observation_3 = CmdOutputObservation(content='', command='ls', command_id=3)
  179. cmd_observation_3._cause = cmd_action_3._id
  180. state.history.append(cmd_observation_3)
  181. # 10 events
  182. assert len(state.history) == 10
  183. assert stuck_detector.is_stuck(headless_mode=True) is False
  184. cmd_action_4 = CmdRunAction(command='ls')
  185. cmd_action_4._id = 4
  186. state.history.append(cmd_action_4)
  187. cmd_observation_4 = CmdOutputObservation(content='', command='ls', command_id=4)
  188. cmd_observation_4._cause = cmd_action_4._id
  189. state.history.append(cmd_observation_4)
  190. # 12 events
  191. assert len(state.history) == 12
  192. with patch('logging.Logger.warning') as mock_warning:
  193. assert stuck_detector.is_stuck(headless_mode=True) is True
  194. mock_warning.assert_called_once_with('Action, Observation loop detected')
  195. def test_is_stuck_repeating_action_error(self, stuck_detector: StuckDetector):
  196. state = stuck_detector.state
  197. # (action, error_observation), not necessarily the same error
  198. message_action = MessageAction(content='Done', wait_for_response=False)
  199. message_action._source = EventSource.USER
  200. hello_action = MessageAction(content='Hello', wait_for_response=False)
  201. hello_observation = NullObservation(content='')
  202. state.history.append(hello_action)
  203. # hello_observation._cause = hello_action._id
  204. state.history.append(hello_observation)
  205. # 2 events
  206. cmd_action_1 = CmdRunAction(command='invalid_command')
  207. state.history.append(cmd_action_1)
  208. error_observation_1 = ErrorObservation(content='Command not found')
  209. # error_observation_1._cause = cmd_action_1._id
  210. state.history.append(error_observation_1)
  211. # 4 events
  212. cmd_action_2 = CmdRunAction(command='invalid_command')
  213. state.history.append(cmd_action_2)
  214. error_observation_2 = ErrorObservation(
  215. content='Command still not found or another error'
  216. )
  217. # error_observation_2._cause = cmd_action_2._id
  218. state.history.append(error_observation_2)
  219. # 6 events
  220. message_null_observation = NullObservation(content='')
  221. state.history.append(message_action)
  222. state.history.append(message_null_observation)
  223. # 8 events
  224. cmd_action_3 = CmdRunAction(command='invalid_command')
  225. state.history.append(cmd_action_3)
  226. error_observation_3 = ErrorObservation(content='Different error')
  227. # error_observation_3._cause = cmd_action_3._id
  228. state.history.append(error_observation_3)
  229. # 10 events
  230. cmd_action_4 = CmdRunAction(command='invalid_command')
  231. state.history.append(cmd_action_4)
  232. error_observation_4 = ErrorObservation(content='Command not found')
  233. # error_observation_4._cause = cmd_action_4._id
  234. state.history.append(error_observation_4)
  235. # 12 events
  236. with patch('logging.Logger.warning') as mock_warning:
  237. assert stuck_detector.is_stuck(headless_mode=True) is True
  238. mock_warning.assert_called_once_with(
  239. 'Action, ErrorObservation loop detected'
  240. )
  241. def test_is_stuck_invalid_syntax_error(self, stuck_detector: StuckDetector):
  242. state = stuck_detector.state
  243. self._impl_syntax_error_events(
  244. state,
  245. error_message='SyntaxError: invalid syntax. Perhaps you forgot a comma?',
  246. random_line=False,
  247. )
  248. with patch('logging.Logger.warning'):
  249. assert stuck_detector.is_stuck(headless_mode=True) is True
  250. def test_is_not_stuck_invalid_syntax_error_random_lines(
  251. self, stuck_detector: StuckDetector
  252. ):
  253. state = stuck_detector.state
  254. self._impl_syntax_error_events(
  255. state,
  256. error_message='SyntaxError: invalid syntax. Perhaps you forgot a comma?',
  257. random_line=True,
  258. )
  259. with patch('logging.Logger.warning'):
  260. assert stuck_detector.is_stuck(headless_mode=True) is False
  261. def test_is_not_stuck_invalid_syntax_error_only_three_incidents(
  262. self, stuck_detector: StuckDetector
  263. ):
  264. state = stuck_detector.state
  265. self._impl_syntax_error_events(
  266. state,
  267. error_message='SyntaxError: invalid syntax. Perhaps you forgot a comma?',
  268. random_line=True,
  269. incidents=3,
  270. )
  271. with patch('logging.Logger.warning'):
  272. assert stuck_detector.is_stuck(headless_mode=True) is False
  273. def test_is_stuck_incomplete_input_error(self, stuck_detector: StuckDetector):
  274. state = stuck_detector.state
  275. self._impl_syntax_error_events(
  276. state,
  277. error_message='SyntaxError: incomplete input',
  278. random_line=False,
  279. )
  280. with patch('logging.Logger.warning'):
  281. assert stuck_detector.is_stuck(headless_mode=True) is True
  282. def test_is_not_stuck_incomplete_input_error(self, stuck_detector: StuckDetector):
  283. state = stuck_detector.state
  284. self._impl_syntax_error_events(
  285. state,
  286. error_message='SyntaxError: incomplete input',
  287. random_line=True,
  288. )
  289. with patch('logging.Logger.warning'):
  290. assert stuck_detector.is_stuck(headless_mode=True) is False
  291. def test_is_not_stuck_ipython_unterminated_string_error_random_lines(
  292. self, stuck_detector: StuckDetector
  293. ):
  294. state = stuck_detector.state
  295. self._impl_unterminated_string_error_events(state, random_line=True)
  296. with patch('logging.Logger.warning'):
  297. assert stuck_detector.is_stuck(headless_mode=True) is False
  298. def test_is_not_stuck_ipython_unterminated_string_error_only_three_incidents(
  299. self, stuck_detector: StuckDetector
  300. ):
  301. state = stuck_detector.state
  302. self._impl_unterminated_string_error_events(
  303. state, random_line=False, incidents=3
  304. )
  305. with patch('logging.Logger.warning'):
  306. assert stuck_detector.is_stuck(headless_mode=True) is False
  307. def test_is_stuck_ipython_unterminated_string_error(
  308. self, stuck_detector: StuckDetector
  309. ):
  310. state = stuck_detector.state
  311. self._impl_unterminated_string_error_events(state, random_line=False)
  312. with patch('logging.Logger.warning'):
  313. assert stuck_detector.is_stuck(headless_mode=True) is True
  314. def test_is_not_stuck_ipython_syntax_error_not_at_end(
  315. self, stuck_detector: StuckDetector
  316. ):
  317. state = stuck_detector.state
  318. # this test is to make sure we don't get false positives
  319. # since the "at line x" is changing in between!
  320. ipython_action_1 = IPythonRunCellAction(code='print("hello')
  321. state.history.append(ipython_action_1)
  322. ipython_observation_1 = IPythonRunCellObservation(
  323. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 1)\nThis is some additional output',
  324. code='print("hello',
  325. )
  326. # ipython_observation_1._cause = ipython_action_1._id
  327. state.history.append(ipython_observation_1)
  328. ipython_action_2 = IPythonRunCellAction(code='print("hello')
  329. state.history.append(ipython_action_2)
  330. ipython_observation_2 = IPythonRunCellObservation(
  331. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 1)\nToo much output here on and on',
  332. code='print("hello',
  333. )
  334. # ipython_observation_2._cause = ipython_action_2._id
  335. state.history.append(ipython_observation_2)
  336. ipython_action_3 = IPythonRunCellAction(code='print("hello')
  337. state.history.append(ipython_action_3)
  338. ipython_observation_3 = IPythonRunCellObservation(
  339. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 3)\nEnough',
  340. code='print("hello',
  341. )
  342. # ipython_observation_3._cause = ipython_action_3._id
  343. state.history.append(ipython_observation_3)
  344. ipython_action_4 = IPythonRunCellAction(code='print("hello')
  345. state.history.append(ipython_action_4)
  346. ipython_observation_4 = IPythonRunCellObservation(
  347. content='print("hello\n ^\nSyntaxError: unterminated string literal (detected at line 2)\nLast line of output',
  348. code='print("hello',
  349. )
  350. # ipython_observation_4._cause = ipython_action_4._id
  351. state.history.append(ipython_observation_4)
  352. with patch('logging.Logger.warning') as mock_warning:
  353. assert stuck_detector.is_stuck(headless_mode=True) is False
  354. mock_warning.assert_not_called()
  355. def test_is_stuck_repeating_action_observation_pattern(
  356. self, stuck_detector: StuckDetector
  357. ):
  358. state = stuck_detector.state
  359. message_action = MessageAction(content='Come on', wait_for_response=False)
  360. message_action._source = EventSource.USER
  361. state.history.append(message_action)
  362. message_observation = NullObservation(content='')
  363. state.history.append(message_observation)
  364. cmd_action_1 = CmdRunAction(command='ls')
  365. state.history.append(cmd_action_1)
  366. cmd_observation_1 = CmdOutputObservation(
  367. command_id=1, command='ls', content='file1.txt\nfile2.txt'
  368. )
  369. # cmd_observation_1._cause = cmd_action_1._id
  370. state.history.append(cmd_observation_1)
  371. read_action_1 = FileReadAction(path='file1.txt')
  372. state.history.append(read_action_1)
  373. read_observation_1 = FileReadObservation(
  374. content='File content', path='file1.txt'
  375. )
  376. # read_observation_1._cause = read_action_1._id
  377. state.history.append(read_observation_1)
  378. cmd_action_2 = CmdRunAction(command='ls')
  379. state.history.append(cmd_action_2)
  380. cmd_observation_2 = CmdOutputObservation(
  381. command_id=2, command='ls', content='file1.txt\nfile2.txt'
  382. )
  383. # cmd_observation_2._cause = cmd_action_2._id
  384. state.history.append(cmd_observation_2)
  385. read_action_2 = FileReadAction(path='file1.txt')
  386. state.history.append(read_action_2)
  387. read_observation_2 = FileReadObservation(
  388. content='File content', path='file1.txt'
  389. )
  390. # read_observation_2._cause = read_action_2._id
  391. state.history.append(read_observation_2)
  392. message_action = MessageAction(content='Come on', wait_for_response=False)
  393. message_action._source = EventSource.USER
  394. state.history.append(message_action)
  395. message_null_observation = NullObservation(content='')
  396. state.history.append(message_null_observation)
  397. cmd_action_3 = CmdRunAction(command='ls')
  398. state.history.append(cmd_action_3)
  399. cmd_observation_3 = CmdOutputObservation(
  400. command_id=3, command='ls', content='file1.txt\nfile2.txt'
  401. )
  402. # cmd_observation_3._cause = cmd_action_3._id
  403. state.history.append(cmd_observation_3)
  404. read_action_3 = FileReadAction(path='file1.txt')
  405. state.history.append(read_action_3)
  406. read_observation_3 = FileReadObservation(
  407. content='File content', path='file1.txt'
  408. )
  409. # read_observation_3._cause = read_action_3._id
  410. state.history.append(read_observation_3)
  411. with patch('logging.Logger.warning') as mock_warning:
  412. assert stuck_detector.is_stuck(headless_mode=True) is True
  413. mock_warning.assert_called_once_with('Action, Observation pattern detected')
  414. def test_is_stuck_not_stuck(self, stuck_detector: StuckDetector):
  415. state = stuck_detector.state
  416. message_action = MessageAction(content='Done', wait_for_response=False)
  417. message_action._source = EventSource.USER
  418. hello_action = MessageAction(content='Hello', wait_for_response=False)
  419. state.history.append(hello_action)
  420. hello_observation = NullObservation(content='')
  421. # hello_observation._cause = hello_action._id
  422. state.history.append(hello_observation)
  423. cmd_action_1 = CmdRunAction(command='ls')
  424. state.history.append(cmd_action_1)
  425. cmd_observation_1 = CmdOutputObservation(
  426. command_id=cmd_action_1.id, command='ls', content='file1.txt\nfile2.txt'
  427. )
  428. # cmd_observation_1._cause = cmd_action_1._id
  429. state.history.append(cmd_observation_1)
  430. read_action_1 = FileReadAction(path='file1.txt')
  431. state.history.append(read_action_1)
  432. read_observation_1 = FileReadObservation(
  433. content='File content', path='file1.txt'
  434. )
  435. # read_observation_1._cause = read_action_1._id
  436. state.history.append(read_observation_1)
  437. cmd_action_2 = CmdRunAction(command='pwd')
  438. state.history.append(cmd_action_2)
  439. cmd_observation_2 = CmdOutputObservation(
  440. command_id=2, command='pwd', content='/home/user'
  441. )
  442. # cmd_observation_2._cause = cmd_action_2._id
  443. state.history.append(cmd_observation_2)
  444. read_action_2 = FileReadAction(path='file2.txt')
  445. state.history.append(read_action_2)
  446. read_observation_2 = FileReadObservation(
  447. content='Another file content', path='file2.txt'
  448. )
  449. # read_observation_2._cause = read_action_2._id
  450. state.history.append(read_observation_2)
  451. message_null_observation = NullObservation(content='')
  452. state.history.append(message_action)
  453. state.history.append(message_null_observation)
  454. cmd_action_3 = CmdRunAction(command='pwd')
  455. state.history.append(cmd_action_3)
  456. cmd_observation_3 = CmdOutputObservation(
  457. command_id=cmd_action_3.id, command='pwd', content='/home/user'
  458. )
  459. # cmd_observation_3._cause = cmd_action_3._id
  460. state.history.append(cmd_observation_3)
  461. read_action_3 = FileReadAction(path='file2.txt')
  462. state.history.append(read_action_3)
  463. read_observation_3 = FileReadObservation(
  464. content='Another file content', path='file2.txt'
  465. )
  466. # read_observation_3._cause = read_action_3._id
  467. state.history.append(read_observation_3)
  468. assert stuck_detector.is_stuck(headless_mode=True) is False
  469. def test_is_stuck_monologue(self, stuck_detector):
  470. state = stuck_detector.state
  471. # Add events to the history list directly
  472. message_action_1 = MessageAction(content='Hi there!')
  473. message_action_1._source = EventSource.USER
  474. state.history.append(message_action_1)
  475. message_action_2 = MessageAction(content='Hi there!')
  476. message_action_2._source = EventSource.AGENT
  477. state.history.append(message_action_2)
  478. message_action_3 = MessageAction(content='How are you?')
  479. message_action_3._source = EventSource.USER
  480. state.history.append(message_action_3)
  481. cmd_kill_action = CmdRunAction(
  482. command='echo 42', thought="I'm not stuck, he's stuck"
  483. )
  484. state.history.append(cmd_kill_action)
  485. message_action_4 = MessageAction(content="I'm doing well, thanks for asking.")
  486. message_action_4._source = EventSource.AGENT
  487. state.history.append(message_action_4)
  488. message_action_5 = MessageAction(content="I'm doing well, thanks for asking.")
  489. message_action_5._source = EventSource.AGENT
  490. state.history.append(message_action_5)
  491. message_action_6 = MessageAction(content="I'm doing well, thanks for asking.")
  492. message_action_6._source = EventSource.AGENT
  493. state.history.append(message_action_6)
  494. assert stuck_detector.is_stuck(headless_mode=True)
  495. # Add an observation event between the repeated message actions
  496. cmd_output_observation = CmdOutputObservation(
  497. content='OK, I was stuck, but no more.',
  498. command_id=42,
  499. command='storybook',
  500. exit_code=0,
  501. )
  502. # cmd_output_observation._cause = cmd_kill_action._id
  503. state.history.append(cmd_output_observation)
  504. message_action_7 = MessageAction(content="I'm doing well, thanks for asking.")
  505. message_action_7._source = EventSource.AGENT
  506. state.history.append(message_action_7)
  507. message_action_8 = MessageAction(content="I'm doing well, thanks for asking.")
  508. message_action_8._source = EventSource.AGENT
  509. state.history.append(message_action_8)
  510. with patch('logging.Logger.warning'):
  511. assert not stuck_detector.is_stuck(headless_mode=True)
  512. class TestAgentController:
  513. @pytest.fixture
  514. def controller(self):
  515. controller = Mock(spec=AgentController)
  516. controller._is_stuck = AgentController._is_stuck.__get__(
  517. controller, AgentController
  518. )
  519. controller.delegate = None
  520. controller.state = Mock()
  521. return controller
  522. def test_is_stuck_delegate_stuck(self, controller: AgentController):
  523. controller.delegate = Mock()
  524. controller.delegate._is_stuck.return_value = True
  525. assert controller._is_stuck() is True