openhands-sdk 1.6.0__tar.gz → 1.7.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 (179) hide show
  1. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/PKG-INFO +2 -2
  2. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/__init__.py +9 -1
  3. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/agent.py +4 -11
  4. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/base.py +11 -0
  5. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
  6. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/utils.py +9 -0
  7. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/__init__.py +2 -0
  8. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/agent_context.py +16 -8
  9. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/prompt.py +40 -2
  10. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
  11. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/__init__.py +2 -0
  12. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/skill.py +61 -0
  13. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/view.py +85 -22
  14. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/conversation.py +13 -0
  15. openhands_sdk-1.7.0/openhands/sdk/conversation/exceptions.py +50 -0
  16. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/local_conversation.py +27 -5
  17. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/remote_conversation.py +101 -3
  18. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/stuck_detector.py +81 -45
  19. openhands_sdk-1.7.0/openhands/sdk/conversation/types.py +45 -0
  20. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/system.py +16 -20
  21. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/message.py +2 -2
  22. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_features.py +64 -24
  23. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/verified_models.py +4 -4
  24. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/logger/logger.py +1 -1
  25. openhands_sdk-1.7.0/openhands/sdk/utils/async_executor.py +115 -0
  26. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/models.py +1 -1
  27. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/PKG-INFO +2 -2
  28. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/requires.txt +1 -1
  29. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/pyproject.toml +2 -2
  30. openhands_sdk-1.6.0/openhands/sdk/conversation/exceptions.py +0 -25
  31. openhands_sdk-1.6.0/openhands/sdk/conversation/types.py +0 -15
  32. openhands_sdk-1.6.0/openhands/sdk/utils/async_executor.py +0 -106
  33. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/__init__.py +0 -0
  34. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  35. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  36. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  37. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  38. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
  39. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  40. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  41. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  42. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  43. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  44. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  45. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  46. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  47. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  48. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/base.py +0 -0
  49. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  50. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  51. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  52. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  53. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  54. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  55. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  56. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/exceptions.py +0 -0
  57. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/trigger.py +0 -0
  58. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/context/skills/types.py +0 -0
  59. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/__init__.py +0 -0
  60. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/base.py +0 -0
  61. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  62. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/event_store.py +0 -0
  63. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  64. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  65. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  66. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  67. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/response_utils.py +0 -0
  68. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  69. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
  70. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/state.py +0 -0
  71. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/title_utils.py +0 -0
  72. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  73. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  74. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  75. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/__init__.py +0 -0
  76. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/base.py +0 -0
  77. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  78. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  79. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  80. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  81. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/__init__.py +0 -0
  82. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/base.py +0 -0
  83. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/condenser.py +0 -0
  84. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_error.py +0 -0
  85. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/conversation_state.py +0 -0
  86. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  87. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  88. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  89. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  90. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  91. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/token.py +0 -0
  92. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/types.py +0 -0
  93. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/event/user_action.py +0 -0
  94. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/exceptions.py +0 -0
  95. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/git_changes.py +0 -0
  96. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/git_diff.py +0 -0
  97. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/models.py +0 -0
  98. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/git/utils.py +0 -0
  99. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/__init__.py +0 -0
  100. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/base.py +0 -0
  101. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/local.py +0 -0
  102. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/io/memory.py +0 -0
  103. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/__init__.py +0 -0
  104. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  105. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  106. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  107. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  108. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm.py +0 -0
  109. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_registry.py +0 -0
  110. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/llm_response.py +0 -0
  111. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  112. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  113. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/__init__.py +0 -0
  114. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  115. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/common.py +0 -0
  116. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  117. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/__init__.py +0 -0
  118. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/base.py +0 -0
  119. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  120. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  121. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/streaming.py +0 -0
  122. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  123. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  124. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  125. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  126. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  127. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  128. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/logger/__init__.py +0 -0
  129. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/logger/rolling.py +0 -0
  130. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/__init__.py +0 -0
  131. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/client.py +0 -0
  132. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/definition.py +0 -0
  133. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/exceptions.py +0 -0
  134. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/tool.py +0 -0
  135. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/mcp/utils.py +0 -0
  136. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/__init__.py +0 -0
  137. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/laminar.py +0 -0
  138. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/observability/utils.py +0 -0
  139. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/py.typed +0 -0
  140. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/secret/__init__.py +0 -0
  141. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/secret/secrets.py +0 -0
  142. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/__init__.py +0 -0
  143. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/analyzer.py +0 -0
  144. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  145. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  146. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/security/risk.py +0 -0
  147. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/__init__.py +0 -0
  148. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  149. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  150. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/builtins/think.py +0 -0
  151. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/registry.py +0 -0
  152. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/schema.py +0 -0
  153. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/spec.py +0 -0
  154. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/tool/tool.py +0 -0
  155. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/__init__.py +0 -0
  156. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/async_utils.py +0 -0
  157. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/cipher.py +0 -0
  158. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/command.py +0 -0
  159. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/deprecation.py +0 -0
  160. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/github.py +0 -0
  161. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/json.py +0 -0
  162. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/paging.py +0 -0
  163. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
  164. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  165. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/truncate.py +0 -0
  166. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/utils/visualize.py +0 -0
  167. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/__init__.py +0 -0
  168. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/base.py +0 -0
  169. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/local.py +0 -0
  170. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/models.py +0 -0
  171. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  172. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  173. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/base.py +0 -0
  174. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  175. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands/sdk/workspace/workspace.py +0 -0
  176. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
  177. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  178. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  179. {openhands_sdk-1.6.0 → openhands_sdk-1.7.0}/setup.cfg +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.6.0
3
+ Version: 1.7.0
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: deprecation>=2.1.0
7
7
  Requires-Dist: fastmcp>=2.11.3
8
8
  Requires-Dist: httpx>=0.27.0
9
- Requires-Dist: litellm>=1.80.7
9
+ Requires-Dist: litellm>=1.80.10
10
10
  Requires-Dist: pydantic>=2.11.7
11
11
  Requires-Dist: python-frontmatter>=1.1.0
12
12
  Requires-Dist: python-json-logger>=3.3.0
@@ -1,7 +1,12 @@
1
1
  from importlib.metadata import PackageNotFoundError, version
2
2
 
3
3
  from openhands.sdk.agent import Agent, AgentBase
4
- from openhands.sdk.context import AgentContext
4
+ from openhands.sdk.context import (
5
+ AgentContext,
6
+ load_project_skills,
7
+ load_skills_from_dir,
8
+ load_user_skills,
9
+ )
5
10
  from openhands.sdk.context.condenser import (
6
11
  LLMSummarizingCondenser,
7
12
  )
@@ -99,5 +104,8 @@ __all__ = [
99
104
  "Workspace",
100
105
  "LocalWorkspace",
101
106
  "RemoteWorkspace",
107
+ "load_project_skills",
108
+ "load_skills_from_dir",
109
+ "load_user_skills",
102
110
  "__version__",
103
111
  ]
@@ -109,17 +109,10 @@ class Agent(AgentBase):
109
109
  event = SystemPromptEvent(
110
110
  source="agent",
111
111
  system_prompt=TextContent(text=self.system_message),
112
- # Always expose a 'security_risk' parameter in tool schemas.
113
- # This ensures the schema remains consistent, even if the
114
- # security analyzer is disabled. Validation of this field
115
- # happens dynamically at runtime depending on the analyzer
116
- # configured. This allows weaker models to omit risk field
117
- # and bypass validation requirements when analyzer is disabled.
118
- # For detailed logic, see `_extract_security_risk` method.
119
- tools=[
120
- t.to_openai_tool(add_security_risk_prediction=True)
121
- for t in self.tools_map.values()
122
- ],
112
+ # Tools are stored as ToolDefinition objects and converted to
113
+ # OpenAI format with security_risk parameter during LLM completion.
114
+ # See make_llm_completion() in agent/utils.py for details.
115
+ tools=list(self.tools_map.values()),
123
116
  )
124
117
  on_event(event)
125
118
 
@@ -121,6 +121,15 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
121
121
  "- An absolute path (e.g., '/path/to/custom_prompt.j2')"
122
122
  ),
123
123
  )
124
+ security_policy_filename: str = Field(
125
+ default="security_policy.j2",
126
+ description=(
127
+ "Security policy template filename. Can be either:\n"
128
+ "- A relative filename (e.g., 'security_policy.j2') loaded from the "
129
+ "agent's prompts directory\n"
130
+ "- An absolute path (e.g., '/path/to/custom_security_policy.j2')"
131
+ ),
132
+ )
124
133
  system_prompt_kwargs: dict[str, object] = Field(
125
134
  default_factory=dict,
126
135
  description="Optional kwargs to pass to the system prompt Jinja2 template.",
@@ -165,6 +174,8 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
165
174
  def system_message(self) -> str:
166
175
  """Compute system message on-demand to maintain statelessness."""
167
176
  template_kwargs = dict(self.system_prompt_kwargs)
177
+ # Add security_policy_filename to template kwargs
178
+ template_kwargs["security_policy_filename"] = self.security_policy_filename
168
179
  template_kwargs.setdefault("model_name", self.llm.model)
169
180
  if (
170
181
  "model_family" not in template_kwargs
@@ -74,7 +74,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
74
74
  </SELF_DOCUMENTATION>
75
75
 
76
76
  <SECURITY>
77
- {% include 'security_policy.j2' %}
77
+ {% include security_policy_filename %}
78
78
  </SECURITY>
79
79
 
80
80
  {% if llm_security_analyzer %}
@@ -195,6 +195,15 @@ def make_llm_completion(
195
195
 
196
196
  Returns:
197
197
  LLMResponse from the LLM completion call
198
+
199
+ Note:
200
+ Always exposes a 'security_risk' parameter in tool schemas via
201
+ add_security_risk_prediction=True. This ensures the schema remains
202
+ consistent, even if the security analyzer is disabled. Validation of
203
+ this field happens dynamically at runtime depending on the analyzer
204
+ configured. This allows weaker models to omit risk field and bypass
205
+ validation requirements when analyzer is disabled. For detailed logic,
206
+ see `_extract_security_risk` method in agent.py.
198
207
  """
199
208
  if llm.uses_responses_api():
200
209
  return llm.responses(
@@ -7,6 +7,7 @@ from openhands.sdk.context.skills import (
7
7
  SkillKnowledge,
8
8
  SkillValidationError,
9
9
  TaskTrigger,
10
+ load_project_skills,
10
11
  load_skills_from_dir,
11
12
  load_user_skills,
12
13
  )
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "SkillKnowledge",
22
23
  "load_skills_from_dir",
23
24
  "load_user_skills",
25
+ "load_project_skills",
24
26
  "render_template",
25
27
  "SkillValidationError",
26
28
  ]
@@ -14,7 +14,7 @@ from openhands.sdk.context.skills import (
14
14
  )
15
15
  from openhands.sdk.llm import Message, TextContent
16
16
  from openhands.sdk.logger import get_logger
17
- from openhands.sdk.secret import SecretValue
17
+ from openhands.sdk.secret import SecretSource, SecretValue
18
18
 
19
19
 
20
20
  logger = get_logger(__name__)
@@ -136,15 +136,23 @@ class AgentContext(BaseModel):
136
136
  logger.warning(f"Failed to load public skills: {str(e)}")
137
137
  return self
138
138
 
139
- def get_secret_names(self) -> list[str]:
140
- """Get the list of secret names from the secrets field.
139
+ def get_secret_infos(self) -> list[dict[str, str]]:
140
+ """Get secret information (name and description) from the secrets field.
141
141
 
142
142
  Returns:
143
- List of secret names. Returns an empty list if no secrets are configured.
143
+ List of dictionaries with 'name' and 'description' keys.
144
+ Returns an empty list if no secrets are configured.
145
+ Description will be None if not available.
144
146
  """
145
147
  if not self.secrets:
146
148
  return []
147
- return list(self.secrets.keys())
149
+ secret_infos = []
150
+ for name, secret_value in self.secrets.items():
151
+ description = None
152
+ if isinstance(secret_value, SecretSource):
153
+ description = secret_value.description
154
+ secret_infos.append({"name": name, "description": description})
155
+ return secret_infos
148
156
 
149
157
  def get_system_message_suffix(self) -> str | None:
150
158
  """Get the system message with repo skill content and custom suffix.
@@ -158,15 +166,15 @@ class AgentContext(BaseModel):
158
166
  repo_skills = [s for s in self.skills if s.trigger is None]
159
167
  logger.debug(f"Triggered {len(repo_skills)} repository skills: {repo_skills}")
160
168
  # Build the workspace context information
161
- secret_names = self.get_secret_names()
162
- if repo_skills or self.system_message_suffix or secret_names:
169
+ secret_infos = self.get_secret_infos()
170
+ if repo_skills or self.system_message_suffix or secret_infos:
163
171
  # TODO(test): add a test for this rendering to make sure they work
164
172
  formatted_text = render_template(
165
173
  prompt_dir=str(PROMPT_DIR),
166
174
  template_name="system_message_suffix.j2",
167
175
  repo_skills=repo_skills,
168
176
  system_message_suffix=self.system_message_suffix or "",
169
- secret_names=secret_names,
177
+ secret_infos=secret_infos,
170
178
  ).strip()
171
179
  return formatted_text
172
180
  elif self.system_message_suffix and self.system_message_suffix.strip():
@@ -4,7 +4,45 @@ import re
4
4
  import sys
5
5
  from functools import lru_cache
6
6
 
7
- from jinja2 import Environment, FileSystemBytecodeCache, FileSystemLoader, Template
7
+ from jinja2 import (
8
+ BaseLoader,
9
+ Environment,
10
+ FileSystemBytecodeCache,
11
+ Template,
12
+ TemplateNotFound,
13
+ )
14
+
15
+
16
+ class FlexibleFileSystemLoader(BaseLoader):
17
+ """A Jinja2 loader that supports both relative paths (within a base directory)
18
+ and absolute paths anywhere on the filesystem.
19
+ """
20
+
21
+ def __init__(self, searchpath: str):
22
+ self.searchpath = os.path.abspath(searchpath)
23
+
24
+ def get_source(self, environment, template): # noqa: ARG002
25
+ # If template is an absolute path, use it directly
26
+ if os.path.isabs(template):
27
+ path = template
28
+ else:
29
+ # Otherwise, look for it in the searchpath
30
+ path = os.path.join(self.searchpath, template)
31
+
32
+ if not os.path.exists(path):
33
+ raise TemplateNotFound(template)
34
+
35
+ mtime = os.path.getmtime(path)
36
+ with open(path, encoding="utf-8") as f:
37
+ source = f.read()
38
+
39
+ def uptodate():
40
+ try:
41
+ return os.path.getmtime(path) == mtime
42
+ except OSError:
43
+ return False
44
+
45
+ return source, path, uptodate
8
46
 
9
47
 
10
48
  def refine(text: str) -> str:
@@ -27,7 +65,7 @@ def _get_env(prompt_dir: str) -> Environment:
27
65
  os.makedirs(cache_folder, exist_ok=True)
28
66
  bcc = FileSystemBytecodeCache(directory=cache_folder)
29
67
  env = Environment(
30
- loader=FileSystemLoader(prompt_dir),
68
+ loader=FlexibleFileSystemLoader(prompt_dir),
31
69
  bytecode_cache=bcc,
32
70
  autoescape=False,
33
71
  )
@@ -14,7 +14,7 @@ Please follow them while working.
14
14
 
15
15
  {{ system_message_suffix }}
16
16
  {% endif %}
17
- {% if secret_names %}
17
+ {% if secret_infos %}
18
18
  <CUSTOM_SECRETS>
19
19
  ### Credential Access
20
20
  * Automatic secret injection: When you reference a registered secret key in your bash command, the secret value will be automatically exported as an environment variable before your command executes.
@@ -25,8 +25,8 @@ Please follow them while working.
25
25
  * If it still fails, report it to the user.
26
26
 
27
27
  You have access to the following environment variables
28
- {% for secret_name in secret_names %}
29
- * **${{ secret_name }}**
28
+ {% for secret_info in secret_infos %}
29
+ * **${{ secret_info.name }}**{% if secret_info.description %} - {{ secret_info.description }}{% endif %}
30
30
  {% endfor %}
31
31
  </CUSTOM_SECRETS>
32
32
  {% endif %}
@@ -1,6 +1,7 @@
1
1
  from openhands.sdk.context.skills.exceptions import SkillValidationError
2
2
  from openhands.sdk.context.skills.skill import (
3
3
  Skill,
4
+ load_project_skills,
4
5
  load_public_skills,
5
6
  load_skills_from_dir,
6
7
  load_user_skills,
@@ -21,6 +22,7 @@ __all__ = [
21
22
  "SkillKnowledge",
22
23
  "load_skills_from_dir",
23
24
  "load_user_skills",
25
+ "load_project_skills",
24
26
  "load_public_skills",
25
27
  "SkillValidationError",
26
28
  ]
@@ -398,6 +398,67 @@ def load_user_skills() -> list[Skill]:
398
398
  return all_skills
399
399
 
400
400
 
401
+ def load_project_skills(work_dir: str | Path) -> list[Skill]:
402
+ """Load skills from project-specific directories.
403
+
404
+ Searches for skills in {work_dir}/.openhands/skills/ and
405
+ {work_dir}/.openhands/microagents/ (legacy). Skills from both
406
+ directories are merged, with skills/ taking precedence for
407
+ duplicate names.
408
+
409
+ Args:
410
+ work_dir: Path to the project/working directory.
411
+
412
+ Returns:
413
+ List of Skill objects loaded from project directories.
414
+ Returns empty list if no skills found or loading fails.
415
+ """
416
+ if isinstance(work_dir, str):
417
+ work_dir = Path(work_dir)
418
+
419
+ all_skills = []
420
+ seen_names = set()
421
+
422
+ # Load project-specific skills from .openhands/skills and legacy microagents
423
+ project_skills_dirs = [
424
+ work_dir / ".openhands" / "skills",
425
+ work_dir / ".openhands" / "microagents", # Legacy support
426
+ ]
427
+
428
+ for project_skills_dir in project_skills_dirs:
429
+ if not project_skills_dir.exists():
430
+ logger.debug(
431
+ f"Project skills directory does not exist: {project_skills_dir}"
432
+ )
433
+ continue
434
+
435
+ try:
436
+ logger.debug(f"Loading project skills from {project_skills_dir}")
437
+ repo_skills, knowledge_skills = load_skills_from_dir(project_skills_dir)
438
+
439
+ # Merge repo and knowledge skills
440
+ for skills_dict in [repo_skills, knowledge_skills]:
441
+ for name, skill in skills_dict.items():
442
+ if name not in seen_names:
443
+ all_skills.append(skill)
444
+ seen_names.add(name)
445
+ else:
446
+ logger.warning(
447
+ f"Skipping duplicate skill '{name}' from "
448
+ f"{project_skills_dir}"
449
+ )
450
+
451
+ except Exception as e:
452
+ logger.warning(
453
+ f"Failed to load project skills from {project_skills_dir}: {str(e)}"
454
+ )
455
+
456
+ logger.debug(
457
+ f"Loaded {len(all_skills)} project skills: {[s.name for s in all_skills]}"
458
+ )
459
+ return all_skills
460
+
461
+
401
462
  # Public skills repository configuration
402
463
  PUBLIC_SKILLS_REPO = "https://github.com/OpenHands/skills"
403
464
  PUBLIC_SKILLS_BRANCH = "main"
@@ -89,38 +89,72 @@ class View(BaseModel):
89
89
  raise ValueError(f"Invalid key type: {type(key)}")
90
90
 
91
91
  @staticmethod
92
- def _enforce_batch_atomicity(
92
+ def _build_action_batches(
93
93
  events: Sequence[Event],
94
- forgotten_event_ids: set[EventID],
95
- ) -> set[EventID]:
96
- """Ensure that if any event in a batch is forgotten, all events in that
97
- batch are forgotten.
98
-
99
- This prevents partial batches from being sent to the LLM, which can cause
100
- API errors when thinking blocks are separated from their tool calls.
94
+ ) -> tuple[
95
+ dict[EventID, list[EventID]], dict[EventID, EventID], dict[EventID, ToolCallID]
96
+ ]:
97
+ """Build a map of llm_response_id -> list of ActionEvent IDs.
98
+
99
+ Returns:
100
+ A tuple of:
101
+ - batches: dict mapping llm_response_id to list of ActionEvent IDs
102
+ - action_id_to_response_id: dict mapping ActionEvent ID to llm_response_id
103
+ - action_id_to_tool_call_id: dict mapping ActionEvent ID to tool_call_id
101
104
  """
102
105
  batches: dict[EventID, list[EventID]] = {}
106
+ action_id_to_response_id: dict[EventID, EventID] = {}
107
+ action_id_to_tool_call_id: dict[EventID, ToolCallID] = {}
108
+
103
109
  for event in events:
104
110
  if isinstance(event, ActionEvent):
105
111
  llm_response_id = event.llm_response_id
106
112
  if llm_response_id not in batches:
107
113
  batches[llm_response_id] = []
108
114
  batches[llm_response_id].append(event.id)
115
+ action_id_to_response_id[event.id] = llm_response_id
116
+ action_id_to_tool_call_id[event.id] = event.tool_call_id
117
+
118
+ return batches, action_id_to_response_id, action_id_to_tool_call_id
119
+
120
+ @staticmethod
121
+ def _enforce_batch_atomicity(
122
+ events: Sequence[Event],
123
+ removed_event_ids: set[EventID],
124
+ ) -> set[EventID]:
125
+ """Ensure that if any ActionEvent in a batch is removed, all ActionEvents
126
+ in that batch are removed.
127
+
128
+ This prevents partial batches from being sent to the LLM, which can cause
129
+ API errors when thinking blocks are separated from their tool calls.
109
130
 
110
- updated_forgotten_ids = set(forgotten_event_ids)
131
+ Args:
132
+ events: The original list of events
133
+ removed_event_ids: Set of event IDs that are being removed
134
+
135
+ Returns:
136
+ Updated set of event IDs that should be removed (including all
137
+ ActionEvents in batches where any ActionEvent was removed)
138
+ """
139
+ batches, action_id_to_response_id, _ = View._build_action_batches(events)
140
+
141
+ if not batches:
142
+ return removed_event_ids
143
+
144
+ updated_removed_ids = set(removed_event_ids)
111
145
 
112
146
  for llm_response_id, batch_event_ids in batches.items():
113
- # Check if any event in this batch is being forgotten
114
- if any(event_id in forgotten_event_ids for event_id in batch_event_ids):
115
- # If so, forget all events in this batch
116
- updated_forgotten_ids.update(batch_event_ids)
147
+ # Check if any ActionEvent in this batch is being removed
148
+ if any(event_id in removed_event_ids for event_id in batch_event_ids):
149
+ # If so, remove all ActionEvents in this batch
150
+ updated_removed_ids.update(batch_event_ids)
117
151
  logger.debug(
118
- f"Enforcing batch atomicity: forgetting entire batch "
152
+ f"Enforcing batch atomicity: removing entire batch "
119
153
  f"with llm_response_id={llm_response_id} "
120
154
  f"({len(batch_event_ids)} events)"
121
155
  )
122
156
 
123
- return updated_forgotten_ids
157
+ return updated_removed_ids
124
158
 
125
159
  @staticmethod
126
160
  def filter_unmatched_tool_calls(
@@ -129,18 +163,47 @@ class View(BaseModel):
129
163
  """Filter out unmatched tool call events.
130
164
 
131
165
  Removes ActionEvents and ObservationEvents that have tool_call_ids
132
- but don't have matching pairs.
166
+ but don't have matching pairs. Also enforces batch atomicity - if any
167
+ ActionEvent in a batch is filtered out, all ActionEvents in that batch
168
+ are also filtered out.
133
169
  """
134
170
  action_tool_call_ids = View._get_action_tool_call_ids(events)
135
171
  observation_tool_call_ids = View._get_observation_tool_call_ids(events)
136
172
 
137
- return [
138
- event
139
- for event in events
140
- if View._should_keep_event(
173
+ # Build batch info for batch atomicity enforcement
174
+ _, _, action_id_to_tool_call_id = View._build_action_batches(events)
175
+
176
+ # First pass: identify which events would NOT be kept based on matching
177
+ removed_event_ids: set[EventID] = set()
178
+ for event in events:
179
+ if not View._should_keep_event(
141
180
  event, action_tool_call_ids, observation_tool_call_ids
142
- )
143
- ]
181
+ ):
182
+ removed_event_ids.add(event.id)
183
+
184
+ # Second pass: enforce batch atomicity for ActionEvents
185
+ # If any ActionEvent in a batch is removed, all ActionEvents in that
186
+ # batch should also be removed
187
+ removed_event_ids = View._enforce_batch_atomicity(events, removed_event_ids)
188
+
189
+ # Third pass: also remove ObservationEvents whose ActionEvents were removed
190
+ # due to batch atomicity
191
+ tool_call_ids_to_remove: set[ToolCallID] = set()
192
+ for action_id in removed_event_ids:
193
+ if action_id in action_id_to_tool_call_id:
194
+ tool_call_ids_to_remove.add(action_id_to_tool_call_id[action_id])
195
+
196
+ # Filter out removed events
197
+ result = []
198
+ for event in events:
199
+ if event.id in removed_event_ids:
200
+ continue
201
+ if isinstance(event, ObservationBaseEvent):
202
+ if event.tool_call_id in tool_call_ids_to_remove:
203
+ continue
204
+ result.append(event)
205
+
206
+ return result
144
207
 
145
208
  @staticmethod
146
209
  def _get_action_tool_call_ids(events: list[LLMConvertibleEvent]) -> set[ToolCallID]:
@@ -1,3 +1,4 @@
1
+ from collections.abc import Mapping
1
2
  from pathlib import Path
2
3
  from typing import TYPE_CHECKING, Self, overload
3
4
 
@@ -7,6 +8,7 @@ from openhands.sdk.conversation.types import (
7
8
  ConversationCallbackType,
8
9
  ConversationID,
9
10
  ConversationTokenCallbackType,
11
+ StuckDetectionThresholds,
10
12
  )
11
13
  from openhands.sdk.conversation.visualizer import (
12
14
  ConversationVisualizerBase,
@@ -56,6 +58,9 @@ class Conversation:
56
58
  token_callbacks: list[ConversationTokenCallbackType] | None = None,
57
59
  max_iteration_per_run: int = 500,
58
60
  stuck_detection: bool = True,
61
+ stuck_detection_thresholds: (
62
+ StuckDetectionThresholds | Mapping[str, int] | None
63
+ ) = None,
59
64
  visualizer: (
60
65
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
61
66
  ) = DefaultConversationVisualizer,
@@ -73,6 +78,9 @@ class Conversation:
73
78
  token_callbacks: list[ConversationTokenCallbackType] | None = None,
74
79
  max_iteration_per_run: int = 500,
75
80
  stuck_detection: bool = True,
81
+ stuck_detection_thresholds: (
82
+ StuckDetectionThresholds | Mapping[str, int] | None
83
+ ) = None,
76
84
  visualizer: (
77
85
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
78
86
  ) = DefaultConversationVisualizer,
@@ -90,6 +98,9 @@ class Conversation:
90
98
  token_callbacks: list[ConversationTokenCallbackType] | None = None,
91
99
  max_iteration_per_run: int = 500,
92
100
  stuck_detection: bool = True,
101
+ stuck_detection_thresholds: (
102
+ StuckDetectionThresholds | Mapping[str, int] | None
103
+ ) = None,
93
104
  visualizer: (
94
105
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
95
106
  ) = DefaultConversationVisualizer,
@@ -114,6 +125,7 @@ class Conversation:
114
125
  token_callbacks=token_callbacks,
115
126
  max_iteration_per_run=max_iteration_per_run,
116
127
  stuck_detection=stuck_detection,
128
+ stuck_detection_thresholds=stuck_detection_thresholds,
117
129
  visualizer=visualizer,
118
130
  workspace=workspace,
119
131
  secrets=secrets,
@@ -126,6 +138,7 @@ class Conversation:
126
138
  token_callbacks=token_callbacks,
127
139
  max_iteration_per_run=max_iteration_per_run,
128
140
  stuck_detection=stuck_detection,
141
+ stuck_detection_thresholds=stuck_detection_thresholds,
129
142
  visualizer=visualizer,
130
143
  workspace=workspace,
131
144
  persistence_dir=persistence_dir,
@@ -0,0 +1,50 @@
1
+ from openhands.sdk.conversation.types import ConversationID
2
+
3
+
4
+ ISSUE_URL = "https://github.com/OpenHands/software-agent-sdk/issues/new"
5
+
6
+
7
+ class ConversationRunError(RuntimeError):
8
+ """Raised when a conversation run fails.
9
+
10
+ Carries the conversation_id and persistence_dir to make resuming/debugging
11
+ easier while preserving the original exception via exception chaining.
12
+ """
13
+
14
+ conversation_id: ConversationID
15
+ persistence_dir: str | None
16
+ original_exception: BaseException
17
+
18
+ def __init__(
19
+ self,
20
+ conversation_id: ConversationID,
21
+ original_exception: BaseException,
22
+ persistence_dir: str | None = None,
23
+ message: str | None = None,
24
+ ) -> None:
25
+ self.conversation_id = conversation_id
26
+ self.persistence_dir = persistence_dir
27
+ self.original_exception = original_exception
28
+ default_msg = self._build_error_message(
29
+ conversation_id, original_exception, persistence_dir
30
+ )
31
+ super().__init__(message or default_msg)
32
+
33
+ @staticmethod
34
+ def _build_error_message(
35
+ conversation_id: ConversationID,
36
+ original_exception: BaseException,
37
+ persistence_dir: str | None,
38
+ ) -> str:
39
+ """Build a detailed error message with debugging information."""
40
+ lines = [
41
+ f"Conversation run failed for id={conversation_id}: {original_exception}",
42
+ ]
43
+
44
+ if persistence_dir:
45
+ lines.append(f"\nConversation logs are stored at: {persistence_dir}")
46
+ lines.append("\nTo help debug this issue, please file a bug report at:")
47
+ lines.append(f" {ISSUE_URL}")
48
+ lines.append("and attach the conversation logs from the directory above.")
49
+
50
+ return "\n".join(lines)
@@ -18,6 +18,7 @@ from openhands.sdk.conversation.types import (
18
18
  ConversationCallbackType,
19
19
  ConversationID,
20
20
  ConversationTokenCallbackType,
21
+ StuckDetectionThresholds,
21
22
  )
22
23
  from openhands.sdk.conversation.visualizer import (
23
24
  ConversationVisualizerBase,
@@ -66,6 +67,9 @@ class LocalConversation(BaseConversation):
66
67
  token_callbacks: list[ConversationTokenCallbackType] | None = None,
67
68
  max_iteration_per_run: int = 500,
68
69
  stuck_detection: bool = True,
70
+ stuck_detection_thresholds: (
71
+ StuckDetectionThresholds | Mapping[str, int] | None
72
+ ) = None,
69
73
  visualizer: (
70
74
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
71
75
  ) = DefaultConversationVisualizer,
@@ -92,6 +96,11 @@ class LocalConversation(BaseConversation):
92
96
  - ConversationVisualizerBase instance: Use custom visualizer
93
97
  - None: No visualization
94
98
  stuck_detection: Whether to enable stuck detection
99
+ stuck_detection_thresholds: Optional configuration for stuck detection
100
+ thresholds. Can be a StuckDetectionThresholds instance or
101
+ a dict with keys: 'action_observation', 'action_error',
102
+ 'monologue', 'alternating_pattern'. Values are integers
103
+ representing the number of repetitions before triggering.
95
104
  """
96
105
  super().__init__() # Initialize with span tracking
97
106
  # Mark cleanup as initiated as early as possible to avoid races or partially
@@ -159,7 +168,20 @@ class LocalConversation(BaseConversation):
159
168
  self.max_iteration_per_run = max_iteration_per_run
160
169
 
161
170
  # Initialize stuck detector
162
- self._stuck_detector = StuckDetector(self._state) if stuck_detection else None
171
+ if stuck_detection:
172
+ # Convert dict to StuckDetectionThresholds if needed
173
+ if isinstance(stuck_detection_thresholds, Mapping):
174
+ threshold_config = StuckDetectionThresholds(
175
+ **stuck_detection_thresholds
176
+ )
177
+ else:
178
+ threshold_config = stuck_detection_thresholds
179
+ self._stuck_detector = StuckDetector(
180
+ self._state,
181
+ thresholds=threshold_config,
182
+ )
183
+ else:
184
+ self._stuck_detector = None
163
185
 
164
186
  with self._state:
165
187
  self.agent.init_state(self._state, on_event=self._on_event)
@@ -349,10 +371,10 @@ class LocalConversation(BaseConversation):
349
371
  )
350
372
  )
351
373
 
352
- # Re-raise with conversation id for better UX; include original traceback
353
- raise ConversationRunError(self._state.id, e) from e
354
- finally:
355
- self._end_observability_span()
374
+ # Re-raise with conversation id and persistence dir for better UX
375
+ raise ConversationRunError(
376
+ self._state.id, e, persistence_dir=self._state.persistence_dir
377
+ ) from e
356
378
 
357
379
  def set_confirmation_policy(self, policy: ConfirmationPolicyBase) -> None:
358
380
  """Set the confirmation policy and store it in conversation state."""