oagi-core 0.14.2__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. {oagi_core-0.14.2 → oagi_core-0.15.0}/PKG-INFO +1 -1
  2. {oagi_core-0.14.2 → oagi_core-0.15.0}/metapackage/pyproject.toml +2 -2
  3. {oagi_core-0.14.2 → oagi_core-0.15.0}/metapackage/uv.lock +5 -5
  4. {oagi_core-0.14.2 → oagi_core-0.15.0}/pyproject.toml +1 -1
  5. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/oagi.py +13 -9
  6. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/pyautogui_action_handler.py +2 -2
  7. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/ydotool_action_handler.py +2 -2
  8. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/socketio_server.py +1 -1
  9. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/action.py +1 -0
  10. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/output_parser.py +2 -1
  11. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/prompt_builder.py +1 -0
  12. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/conftest.py +16 -0
  13. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_actor.py +21 -0
  14. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_oagi_action_converter.py +18 -0
  15. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_pyautogui_action_handler.py +19 -0
  16. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/utils/test_output_parser.py +25 -0
  17. {oagi_core-0.14.2 → oagi_core-0.15.0}/uv.lock +1 -1
  18. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  19. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  20. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  21. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/ISSUE_TEMPLATE/question.yml +0 -0
  22. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/workflows/ci.yml +0 -0
  23. {oagi_core-0.14.2 → oagi_core-0.15.0}/.github/workflows/release.yml +0 -0
  24. {oagi_core-0.14.2 → oagi_core-0.15.0}/.gitignore +0 -0
  25. {oagi_core-0.14.2 → oagi_core-0.15.0}/.python-version +0 -0
  26. {oagi_core-0.14.2 → oagi_core-0.15.0}/CONTRIBUTING.md +0 -0
  27. {oagi_core-0.14.2 → oagi_core-0.15.0}/LICENSE +0 -0
  28. {oagi_core-0.14.2 → oagi_core-0.15.0}/Makefile +0 -0
  29. {oagi_core-0.14.2 → oagi_core-0.15.0}/README.md +0 -0
  30. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/async_google_weather.py +0 -0
  31. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/execute_task_auto.py +0 -0
  32. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/execute_task_manual.py +0 -0
  33. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/google_weather.py +0 -0
  34. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/multi_screen_execution.py +0 -0
  35. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/openai_agent_loop_example.py +0 -0
  36. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/screenshot_with_config.py +0 -0
  37. {oagi_core-0.14.2 → oagi_core-0.15.0}/examples/tasker_agent_example.py +0 -0
  38. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/__init__.py +0 -0
  39. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/__init__.py +0 -0
  40. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/async_.py +0 -0
  41. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/async_short.py +0 -0
  42. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/base.py +0 -0
  43. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/short.py +0 -0
  44. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/actor/sync.py +0 -0
  45. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/__init__.py +0 -0
  46. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/default.py +0 -0
  47. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/factories.py +0 -0
  48. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/__init__.py +0 -0
  49. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/agent_observer.py +0 -0
  50. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/events.py +0 -0
  51. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/exporters.py +0 -0
  52. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/protocol.py +0 -0
  53. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/observer/report_template.html +0 -0
  54. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/protocol.py +0 -0
  55. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/registry.py +0 -0
  56. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/__init__.py +0 -0
  57. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/memory.py +0 -0
  58. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/models.py +0 -0
  59. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/planner.py +0 -0
  60. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/taskee_agent.py +0 -0
  61. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/agent/tasker/tasker_agent.py +0 -0
  62. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/__init__.py +0 -0
  63. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/agent.py +0 -0
  64. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/display.py +0 -0
  65. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/main.py +0 -0
  66. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/server.py +0 -0
  67. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/tracking.py +0 -0
  68. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/cli/utils.py +0 -0
  69. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/__init__.py +0 -0
  70. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/async_.py +0 -0
  71. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/base.py +0 -0
  72. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/client/sync.py +0 -0
  73. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/constants.py +0 -0
  74. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/__init__.py +0 -0
  75. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/converters/base.py +0 -0
  76. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/exceptions.py +0 -0
  77. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/__init__.py +0 -0
  78. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_macos.py +0 -0
  79. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_windows.py +0 -0
  80. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/_ydotool.py +0 -0
  81. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_pyautogui_action_handler.py +0 -0
  82. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_screenshot_maker.py +0 -0
  83. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/async_ydotool_action_handler.py +0 -0
  84. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/capslock_manager.py +0 -0
  85. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/pil_image.py +0 -0
  86. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/screen_manager.py +0 -0
  87. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/screenshot_maker.py +0 -0
  88. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/utils.py +0 -0
  89. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/handler/wayland_support.py +0 -0
  90. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/logging.py +0 -0
  91. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/platform_info.py +0 -0
  92. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/__init__.py +0 -0
  93. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/agent_wrappers.py +0 -0
  94. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/config.py +0 -0
  95. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/main.py +0 -0
  96. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/models.py +0 -0
  97. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/server/session_store.py +0 -0
  98. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/task/__init__.py +0 -0
  99. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/__init__.py +0 -0
  100. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/action_handler.py +0 -0
  101. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/async_action_handler.py +0 -0
  102. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/async_image_provider.py +0 -0
  103. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/image.py +0 -0
  104. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/image_provider.py +0 -0
  105. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/__init__.py +0 -0
  106. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/client.py +0 -0
  107. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/image_config.py +0 -0
  108. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/models/step.py +0 -0
  109. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/step_observer.py +0 -0
  110. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/types/url.py +0 -0
  111. {oagi_core-0.14.2 → oagi_core-0.15.0}/src/oagi/utils/__init__.py +0 -0
  112. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/__init__.py +0 -0
  113. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_action_parsing.py +0 -0
  114. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent/test_agent_wrappers.py +0 -0
  115. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent/test_default_agent.py +0 -0
  116. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_agent_registry.py +0 -0
  117. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_actor.py +0 -0
  118. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_client.py +0 -0
  119. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_async_handlers.py +0 -0
  120. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_cli.py +0 -0
  121. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_logging.py +0 -0
  122. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_mac_double_click.py +0 -0
  123. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_observer.py +0 -0
  124. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_pil_image.py +0 -0
  125. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_planner.py +0 -0
  126. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_planner_memory.py +0 -0
  127. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_screenshot_maker.py +0 -0
  128. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/__init__.py +0 -0
  129. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_config.py +0 -0
  130. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_session_store.py +0 -0
  131. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_server/test_socketio_integration.py +0 -0
  132. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_sync_client.py +0 -0
  133. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_taskee_agent.py +0 -0
  134. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/test_tasker_agent.py +0 -0
  135. {oagi_core-0.14.2 → oagi_core-0.15.0}/tests/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: oagi-core
3
- Version: 0.14.2
3
+ Version: 0.15.0
4
4
  Summary: Official API of OpenAGI Foundation
5
5
  Project-URL: Homepage, https://github.com/agiopen-org/oagi
6
6
  Author-email: OpenAGI Foundation <contact@agiopen.org>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi"
7
- version = "0.14.2"
7
+ version = "0.15.0"
8
8
  description = "Official API of OpenAGI Foundation (metapackage with all features)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -16,7 +16,7 @@ authors = [
16
16
  requires-python = ">= 3.10"
17
17
 
18
18
  dependencies = [
19
- "oagi-core[desktop,server]==0.14.2",
19
+ "oagi-core[desktop,server]==0.15.0",
20
20
  ]
21
21
 
22
22
  [project.urls]
@@ -527,18 +527,18 @@ wheels = [
527
527
 
528
528
  [[package]]
529
529
  name = "oagi"
530
- version = "0.14.2"
530
+ version = "0.15.0"
531
531
  source = { editable = "." }
532
532
  dependencies = [
533
533
  { name = "oagi-core", extra = ["desktop", "server"] },
534
534
  ]
535
535
 
536
536
  [package.metadata]
537
- requires-dist = [{ name = "oagi-core", extras = ["desktop", "server"], specifier = "==0.14.1" }]
537
+ requires-dist = [{ name = "oagi-core", extras = ["desktop", "server"], specifier = "==0.14.2" }]
538
538
 
539
539
  [[package]]
540
540
  name = "oagi-core"
541
- version = "0.14.1"
541
+ version = "0.14.2"
542
542
  source = { registry = "https://pypi.org/simple" }
543
543
  dependencies = [
544
544
  { name = "httpx" },
@@ -546,9 +546,9 @@ dependencies = [
546
546
  { name = "pydantic" },
547
547
  { name = "rich" },
548
548
  ]
549
- sdist = { url = "https://files.pythonhosted.org/packages/94/20/3dfff9883e8786fc0b79ea915ba83cfbce3addd3149568601b80c3bc83a3/oagi_core-0.14.1.tar.gz", hash = "sha256:407faa59b3ced203901a1b35a5b3bc25407e8b61f82ab9b202f01412c73175bf", size = 310008, upload-time = "2026-01-23T08:09:08.54Z" }
549
+ sdist = { url = "https://files.pythonhosted.org/packages/7f/6e/02d64ba32075a3977e1037228580d29de7122e8d5dce2a1267c71d8c3d69/oagi_core-0.14.2.tar.gz", hash = "sha256:2eda6ef21276dc0c7256b6014f8db7045eaedf98a6f091f0aedf3f44b20321e6", size = 319639, upload-time = "2026-02-03T07:42:15.596Z" }
550
550
  wheels = [
551
- { url = "https://files.pythonhosted.org/packages/62/d1/4485bbddbdf1868aa46deaa98d49ad4624e243c9b32321c81374402bcde8/oagi_core-0.14.1-py3-none-any.whl", hash = "sha256:36b0e10a79c0508eedd28f8a5d89230b7236452ed64bed7150b22f99a50f1a18", size = 114938, upload-time = "2026-01-23T08:09:07.4Z" },
551
+ { url = "https://files.pythonhosted.org/packages/0b/90/62215c6e46961d29d6e9e715d89483d108a811f5c1c854a5bb7b053eacab/oagi_core-0.14.2-py3-none-any.whl", hash = "sha256:ceaeabbe20bafef7c031f3932d6870b42e7406173030790a5a55965b8ebe5f73", size = 124410, upload-time = "2026-02-03T07:42:14.23Z" },
552
552
  ]
553
553
 
554
554
  [package.optional-dependencies]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "oagi-core"
7
- version = "0.14.2"
7
+ version = "0.15.0"
8
8
  description = "Official API of OpenAGI Foundation"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -51,21 +51,21 @@ class OagiActionConverter(BaseActionConverter[Action]):
51
51
  """
52
52
  converted: list[str] = []
53
53
  failed: list[tuple[str, str]] = []
54
- has_finish = False
54
+ has_terminal = False
55
55
 
56
56
  if not actions:
57
57
  return converted
58
58
 
59
59
  for action in actions:
60
- # Check for duplicate finish() during iteration
61
- is_finish = action.type == ActionType.FINISH
62
- if is_finish:
63
- if has_finish:
60
+ # Check for duplicate finish()/fail() during iteration
61
+ is_terminal = action.type in (ActionType.FINISH, ActionType.FAIL)
62
+ if is_terminal:
63
+ if has_terminal:
64
64
  raise ValueError(
65
- "Duplicate finish() detected. "
66
- "Only one finish() is allowed per action sequence."
65
+ "Duplicate finish()/fail() detected. "
66
+ "Only one finish() or fail() is allowed per action sequence."
67
67
  )
68
- has_finish = True
68
+ has_terminal = True
69
69
 
70
70
  try:
71
71
  converted.extend(self._convert_action(action))
@@ -172,6 +172,10 @@ class OagiActionConverter(BaseActionConverter[Action]):
172
172
  self._log_info("Task completion action -> DONE")
173
173
  return ["DONE"]
174
174
 
175
+ if action_type == ActionType.FAIL.value:
176
+ self._log_info("Task infeasible action -> FAIL")
177
+ return ["FAIL"]
178
+
175
179
  if action_type == ActionType.CALL_USER.value:
176
180
  self._log_info("User intervention requested")
177
181
  return []
@@ -179,7 +183,7 @@ class OagiActionConverter(BaseActionConverter[Action]):
179
183
  raise ValueError(
180
184
  f"Unknown action type: '{action_type}'. "
181
185
  "Supported: click, left_double, left_triple, right_single, drag, "
182
- "hotkey, type, scroll, wait, finish, call_user"
186
+ "hotkey, type, scroll, wait, finish, fail, call_user"
183
187
  )
184
188
 
185
189
  def serialize_actions(self, actions: list[Action]) -> list[dict[str, Any]]:
@@ -250,8 +250,8 @@ class PyautoguiActionHandler:
250
250
  )
251
251
  pyautogui.scroll(scroll_amount)
252
252
 
253
- case ActionType.FINISH:
254
- # Task completion - reset handler state
253
+ case ActionType.FINISH | ActionType.FAIL:
254
+ # Task completion or infeasible - reset handler state
255
255
  self.reset()
256
256
 
257
257
  case ActionType.WAIT:
@@ -164,8 +164,8 @@ class YdotoolActionHandler(Ydotool):
164
164
  text = self.caps_manager.transform_text(text)
165
165
  self._run_ydotool(["type", text], count=count)
166
166
 
167
- case ActionType.FINISH:
168
- # Task completion - reset handler state
167
+ case ActionType.FINISH | ActionType.FAIL:
168
+ # Task completion or infeasible - reset handler state
169
169
  self.reset()
170
170
 
171
171
  case ActionType.WAIT:
@@ -364,7 +364,7 @@ class SessionNamespace(socketio.AsyncNamespace):
364
364
  timeout=self.config.socketio_timeout,
365
365
  )
366
366
 
367
- case ActionType.FINISH:
367
+ case ActionType.FINISH | ActionType.FAIL:
368
368
  return await self.call(
369
369
  "finish",
370
370
  FinishEventData(**common).model_dump(),
@@ -22,6 +22,7 @@ class ActionType(str, Enum):
22
22
  TYPE = "type"
23
23
  SCROLL = "scroll"
24
24
  FINISH = "finish"
25
+ FAIL = "fail"
25
26
  WAIT = "wait"
26
27
  CALL_USER = "call_user"
27
28
 
@@ -45,7 +45,7 @@ def parse_raw_output(raw_output: str) -> Step:
45
45
  parsed_action = _parse_action(action_text.strip())
46
46
  if parsed_action:
47
47
  actions.append(parsed_action)
48
- if parsed_action.type == ActionType.FINISH:
48
+ if parsed_action.type in (ActionType.FINISH, ActionType.FAIL):
49
49
  stop = True
50
50
 
51
51
  return Step(reason=reason, actions=actions, stop=stop)
@@ -105,6 +105,7 @@ def _parse_action(action_text: str) -> Action | None:
105
105
  - scroll(x, y, direction, c) # scroll at position
106
106
  - wait() # wait for a while
107
107
  - finish() # indicate task is finished
108
+ - fail() # indicate task is infeasible
108
109
 
109
110
  Args:
110
111
  action_text: String representation of a single action
@@ -24,6 +24,7 @@ In the action field, you have the following action formats:
24
24
  8. scroll(x, y, direction, c) # scroll the mouse at position (x, y) in the direction of up or down for c times, where x and y are integers normalized between 0 and 1000
25
25
  9. wait() # wait for a while
26
26
  10. finish() # indicate the task is finished
27
+ 11. fail() # indicate the task is infeasible
27
28
 
28
29
  Directly output the text beginning with <|think_start|>, no additional text is needed for this scenario.
29
30
 
@@ -132,6 +132,12 @@ def sample_raw_output_completed():
132
132
  return "<|think_start|>The task has been completed successfully<|think_end|>\n<|action_start|>finish()<|action_end|>"
133
133
 
134
134
 
135
+ @pytest.fixture
136
+ def sample_raw_output_failed():
137
+ """Sample raw output for infeasible task."""
138
+ return "<|think_start|>The task is infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>"
139
+
140
+
135
141
  @pytest.fixture
136
142
  def sample_step(sample_action):
137
143
  """Sample Step object for testing."""
@@ -152,6 +158,16 @@ def completed_step():
152
158
  )
153
159
 
154
160
 
161
+ @pytest.fixture
162
+ def failed_step():
163
+ """Sample failed Step object for infeasible task."""
164
+ return Step(
165
+ reason="The task is infeasible",
166
+ actions=[Action(type=ActionType.FAIL, argument="", count=1)],
167
+ stop=True,
168
+ )
169
+
170
+
155
171
  @pytest.fixture
156
172
  def mock_error_response():
157
173
  """Mock error HTTP response."""
@@ -219,6 +219,27 @@ class TestActorStep:
219
219
  assert len(result.actions) == 1
220
220
  assert result.actions[0].type == ActionType.FINISH
221
221
 
222
+ def test_step_with_failed_response(
223
+ self, actor, failed_step, sample_usage_obj, mock_upload_file_response
224
+ ):
225
+ actor.task_description = "Test task"
226
+ actor.task_id = "task-789"
227
+
228
+ # Setup mocks
229
+ actor.client.put_s3_presigned_url.return_value = mock_upload_file_response
230
+ actor.client.chat_completion.return_value = (
231
+ failed_step,
232
+ "<|think_start|>infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>",
233
+ sample_usage_obj,
234
+ )
235
+
236
+ result = actor.step(b"image bytes")
237
+
238
+ assert result.stop is True
239
+ assert result.reason == "The task is infeasible"
240
+ assert len(result.actions) == 1
241
+ assert result.actions[0].type == ActionType.FAIL
242
+
222
243
  def test_step_handles_exception(self, actor, mock_upload_file_response):
223
244
  actor.task_description = "Test task"
224
245
  actor.client.put_s3_presigned_url.return_value = mock_upload_file_response
@@ -97,6 +97,19 @@ class TestSpecialActions:
97
97
  result = converter([action])
98
98
  assert result[0] == "DONE"
99
99
 
100
+ def test_fail_action(self, converter):
101
+ action = Action(type=ActionType.FAIL, argument="", count=1)
102
+ result = converter([action])
103
+ assert result[0] == "FAIL"
104
+
105
+ def test_duplicate_terminal_actions_raises(self, converter):
106
+ actions = [
107
+ Action(type=ActionType.FINISH, argument="", count=1),
108
+ Action(type=ActionType.FAIL, argument="", count=1),
109
+ ]
110
+ with pytest.raises(ValueError, match="Duplicate finish\\(\\)/fail\\(\\)"):
111
+ converter(actions)
112
+
100
113
 
101
114
  class TestActionStringToStep:
102
115
  def test_pyautogui_command(self, converter):
@@ -114,6 +127,11 @@ class TestActionStringToStep:
114
127
  assert step["type"] == "sleep"
115
128
  assert step["parameters"]["seconds"] == 0
116
129
 
130
+ def test_fail_command(self, converter):
131
+ step = converter.action_string_to_step("FAIL")
132
+ assert step["type"] == "sleep"
133
+ assert step["parameters"]["seconds"] == 0
134
+
117
135
 
118
136
  class TestMultipleActions:
119
137
  def test_action_count(self, converter):
@@ -131,6 +131,11 @@ def test_finish_action(handler, mock_pyautogui):
131
131
  handler([action])
132
132
 
133
133
 
134
+ def test_fail_action(handler, mock_pyautogui):
135
+ action = Action(type=ActionType.FAIL, argument="", count=1)
136
+ handler([action])
137
+
138
+
134
139
  def test_call_user_action(handler, mock_pyautogui, capsys):
135
140
  action = Action(type=ActionType.CALL_USER, argument="", count=1)
136
141
  handler([action])
@@ -453,6 +458,20 @@ class TestHandlerReset:
453
458
  handler([finish_action])
454
459
  assert handler.caps_manager.caps_enabled is False
455
460
 
461
+ def test_fail_action_resets_handler(self, mock_pyautogui):
462
+ config = PyautoguiConfig(capslock_mode="session", post_batch_delay=0)
463
+ handler = PyautoguiActionHandler(config=config)
464
+
465
+ # Enable caps lock
466
+ caps_action = Action(type=ActionType.HOTKEY, argument="capslock", count=1)
467
+ handler([caps_action])
468
+ assert handler.caps_manager.caps_enabled is True
469
+
470
+ # FAIL action should also reset handler
471
+ fail_action = Action(type=ActionType.FAIL, argument="", count=1)
472
+ handler([fail_action])
473
+ assert handler.caps_manager.caps_enabled is False
474
+
456
475
 
457
476
  class TestAsyncHandlerReset:
458
477
  def test_async_handler_reset_delegates_to_sync_handler(self, mock_pyautogui):
@@ -34,6 +34,15 @@ class TestParseRawOutput:
34
34
  assert step.actions[0].type == ActionType.FINISH
35
35
  assert step.stop is True
36
36
 
37
+ def test_parse_fail_action_sets_stop(self):
38
+ raw = "<|think_start|>Task is infeasible<|think_end|>\n<|action_start|>fail()<|action_end|>"
39
+ step = parse_raw_output(raw)
40
+
41
+ assert step.reason == "Task is infeasible"
42
+ assert len(step.actions) == 1
43
+ assert step.actions[0].type == ActionType.FAIL
44
+ assert step.stop is True
45
+
37
46
  def test_parse_multiple_actions_with_ampersand(self):
38
47
  raw = "<|think_start|>Do two things<|think_end|>\n<|action_start|>click(100, 200) & type(hello)<|action_end|>"
39
48
  step = parse_raw_output(raw)
@@ -147,6 +156,7 @@ class TestParseAction:
147
156
  ("type(hello world)", ActionType.TYPE, "hello world"),
148
157
  ("wait()", ActionType.WAIT, ""),
149
158
  ("finish()", ActionType.FINISH, ""),
159
+ ("fail()", ActionType.FAIL, ""),
150
160
  ],
151
161
  )
152
162
  def test_parse_basic_actions(self, action_text, expected_type, expected_arg):
@@ -254,3 +264,18 @@ class TestEdgeCases:
254
264
 
255
265
  assert step.stop is True
256
266
  assert len(step.actions) == 2
267
+
268
+ def test_fail_with_other_actions(self):
269
+ raw = "<|think_start|>Test<|think_end|>\n<|action_start|>click(100, 200) & fail()<|action_end|>"
270
+ step = parse_raw_output(raw)
271
+
272
+ assert step.stop is True
273
+ assert len(step.actions) == 2
274
+ assert step.actions[1].type == ActionType.FAIL
275
+
276
+ def test_multiple_fail_actions_stop_true(self):
277
+ raw = "<|think_start|>Test<|think_end|>\n<|action_start|>fail() & fail()<|action_end|>"
278
+ step = parse_raw_output(raw)
279
+
280
+ assert step.stop is True
281
+ assert len(step.actions) == 2
@@ -544,7 +544,7 @@ wheels = [
544
544
 
545
545
  [[package]]
546
546
  name = "oagi-core"
547
- version = "0.14.2"
547
+ version = "0.15.0"
548
548
  source = { editable = "." }
549
549
  dependencies = [
550
550
  { name = "httpx" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes