openhands-sdk 1.10.0__tar.gz → 1.11.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 (200) hide show
  1. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/agent.py +60 -27
  3. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/base.py +1 -1
  4. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/base.py +36 -3
  5. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +65 -1
  6. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +2 -1
  7. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/skill.py +2 -25
  8. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/conversation.py +5 -0
  9. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/impl/local_conversation.py +19 -13
  10. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/impl/remote_conversation.py +10 -0
  11. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/stuck_detector.py +18 -9
  12. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/__init__.py +16 -0
  13. openhands_sdk-1.11.0/openhands/sdk/llm/auth/__init__.py +28 -0
  14. openhands_sdk-1.11.0/openhands/sdk/llm/auth/credentials.py +157 -0
  15. openhands_sdk-1.11.0/openhands/sdk/llm/auth/openai.py +762 -0
  16. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/llm.py +175 -20
  17. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/options/responses_options.py +8 -7
  18. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/model_features.py +2 -0
  19. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/secret/secrets.py +13 -1
  20. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/remote/base.py +8 -3
  21. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +40 -7
  22. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  23. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands_sdk.egg-info/SOURCES.txt +6 -0
  24. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/pyproject.toml +1 -1
  25. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/__init__.py +0 -0
  26. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/__init__.py +0 -0
  27. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  28. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  29. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  30. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  31. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
  32. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  33. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  34. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  35. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  36. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/system_prompt.j2 +0 -0
  37. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  38. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  39. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  40. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  41. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/agent/utils.py +0 -0
  42. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/__init__.py +0 -0
  43. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/agent_context.py +0 -0
  44. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  45. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  46. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  47. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  48. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/condenser/utils.py +0 -0
  49. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  50. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  51. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  52. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  53. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/__init__.py +0 -0
  54. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/exceptions.py +0 -0
  55. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/trigger.py +0 -0
  56. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/types.py +0 -0
  57. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/skills/utils.py +0 -0
  58. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/context/view.py +0 -0
  59. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/__init__.py +0 -0
  60. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/base.py +0 -0
  61. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  62. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/event_store.py +0 -0
  63. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  64. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/exceptions.py +0 -0
  65. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  66. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  67. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  68. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/response_utils.py +0 -0
  69. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  70. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
  71. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/state.py +0 -0
  72. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/title_utils.py +0 -0
  73. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/types.py +0 -0
  74. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  75. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  76. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  77. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/__init__.py +0 -0
  78. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/base.py +0 -0
  79. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  80. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  81. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  82. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  83. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  84. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  85. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  86. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  87. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  88. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/critic/result.py +0 -0
  89. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/__init__.py +0 -0
  90. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/base.py +0 -0
  91. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/condenser.py +0 -0
  92. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/conversation_error.py +0 -0
  93. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/conversation_state.py +0 -0
  94. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  95. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  96. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  97. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  98. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  99. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  100. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/token.py +0 -0
  101. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/types.py +0 -0
  102. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/event/user_action.py +0 -0
  103. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/cached_repo.py +0 -0
  104. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/exceptions.py +0 -0
  105. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/git_changes.py +0 -0
  106. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/git_diff.py +0 -0
  107. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/models.py +0 -0
  108. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/git/utils.py +0 -0
  109. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/__init__.py +0 -0
  110. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/config.py +0 -0
  111. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  112. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/executor.py +0 -0
  113. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/manager.py +0 -0
  114. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/hooks/types.py +0 -0
  115. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/io/__init__.py +0 -0
  116. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/io/base.py +0 -0
  117. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/io/cache.py +0 -0
  118. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/io/local.py +0 -0
  119. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/io/memory.py +0 -0
  120. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  121. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  122. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  123. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  124. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/llm_registry.py +0 -0
  125. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/llm_response.py +0 -0
  126. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/message.py +0 -0
  127. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  128. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  129. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/options/__init__.py +0 -0
  130. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  131. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/options/common.py +0 -0
  132. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/router/__init__.py +0 -0
  133. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/router/base.py +0 -0
  134. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  135. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  136. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/streaming.py +0 -0
  137. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  138. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  139. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  140. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  141. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  142. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  143. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
  144. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/logger/__init__.py +0 -0
  145. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/logger/logger.py +0 -0
  146. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/logger/rolling.py +0 -0
  147. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/__init__.py +0 -0
  148. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/client.py +0 -0
  149. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/definition.py +0 -0
  150. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/exceptions.py +0 -0
  151. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/tool.py +0 -0
  152. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/mcp/utils.py +0 -0
  153. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/observability/__init__.py +0 -0
  154. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/observability/laminar.py +0 -0
  155. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/observability/utils.py +0 -0
  156. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/plugin/__init__.py +0 -0
  157. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/plugin/fetch.py +0 -0
  158. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/plugin/loader.py +0 -0
  159. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/plugin/plugin.py +0 -0
  160. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/plugin/types.py +0 -0
  161. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/py.typed +0 -0
  162. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/secret/__init__.py +0 -0
  163. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/security/__init__.py +0 -0
  164. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/security/analyzer.py +0 -0
  165. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  166. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  167. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/security/risk.py +0 -0
  168. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/__init__.py +0 -0
  169. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  170. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  171. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/builtins/think.py +0 -0
  172. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/registry.py +0 -0
  173. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/schema.py +0 -0
  174. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/spec.py +0 -0
  175. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/tool/tool.py +0 -0
  176. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/__init__.py +0 -0
  177. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/async_executor.py +0 -0
  178. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/async_utils.py +0 -0
  179. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/cipher.py +0 -0
  180. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/command.py +0 -0
  181. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/deprecation.py +0 -0
  182. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/github.py +0 -0
  183. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/json.py +0 -0
  184. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/models.py +0 -0
  185. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/paging.py +0 -0
  186. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
  187. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  188. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/truncate.py +0 -0
  189. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/utils/visualize.py +0 -0
  190. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/__init__.py +0 -0
  191. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/base.py +0 -0
  192. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/local.py +0 -0
  193. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/models.py +0 -0
  194. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  195. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  196. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands/sdk/workspace/workspace.py +0 -0
  197. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  198. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands_sdk.egg-info/requires.txt +0 -0
  199. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  200. {openhands_sdk-1.10.0 → openhands_sdk-1.11.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.10.0
3
+ Version: 1.11.0
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -67,6 +67,10 @@ from openhands.sdk.tool.builtins import (
67
67
  logger = get_logger(__name__)
68
68
  maybe_init_laminar()
69
69
 
70
+ # Maximum number of events to scan during init_state defensive checks.
71
+ # SystemPromptEvent must appear within this prefix (at index 0 or 1).
72
+ INIT_STATE_PREFIX_SCAN_WINDOW = 3
73
+
70
74
 
71
75
  class Agent(AgentBase):
72
76
  """Main agent implementation for OpenHands.
@@ -102,53 +106,82 @@ class Agent(AgentBase):
102
106
  state: ConversationState,
103
107
  on_event: ConversationCallbackType,
104
108
  ) -> None:
109
+ """Initialize conversation state.
110
+
111
+ Invariants enforced by this method:
112
+ - If a SystemPromptEvent is already present, it must be within the first 3
113
+ events (index 0 or 1 in practice; index 2 is included in the scan window
114
+ to detect a user message appearing before the system prompt).
115
+ - A user MessageEvent should not appear before the SystemPromptEvent.
116
+
117
+ These invariants keep event ordering predictable for downstream components
118
+ (condenser, UI, etc.) and also prevent accidentally materializing the full
119
+ event history during initialization.
120
+ """
105
121
  super().init_state(state, on_event=on_event)
106
- # TODO(openhands): we should add test to test this init_state will actually
107
- # modify state in-place
108
122
 
109
123
  # Defensive check: Analyze state to detect unexpected initialization scenarios
110
124
  # These checks help diagnose issues related to lazy loading and event ordering
111
125
  # See: https://github.com/OpenHands/software-agent-sdk/issues/1785
112
- events = list(state.events)
113
- has_system_prompt = any(isinstance(e, SystemPromptEvent) for e in events)
126
+ #
127
+ # NOTE: len() is O(1) for EventLog (file-backed implementation).
128
+ event_count = len(state.events)
129
+
130
+ # NOTE: state.events is intentionally an EventsListBase (Sequence-like), not
131
+ # a plain list. Avoid materializing the full history via list(state.events)
132
+ # here (conversations can reach 30k+ events).
133
+ #
134
+ # Invariant: when init_state is called, SystemPromptEvent (if present) must be
135
+ # at index 0 or 1.
136
+ #
137
+ # Rationale:
138
+ # - Local conversations start empty and init_state is responsible for adding
139
+ # the SystemPromptEvent as the first event.
140
+ # - Remote conversations may receive an initial ConversationStateUpdateEvent
141
+ # from the agent-server immediately after subscription. In a typical remote
142
+ # session prefix you may see:
143
+ # [ConversationStateUpdateEvent, SystemPromptEvent, MessageEvent, ...]
144
+ #
145
+ # We intentionally only inspect the first few events (cheap for both local and
146
+ # remote) to enforce this invariant.
147
+ prefix_events = state.events[:INIT_STATE_PREFIX_SCAN_WINDOW]
148
+
149
+ has_system_prompt = any(isinstance(e, SystemPromptEvent) for e in prefix_events)
114
150
  has_user_message = any(
115
- isinstance(e, MessageEvent) and e.source == "user" for e in events
151
+ isinstance(e, MessageEvent) and e.source == "user" for e in prefix_events
116
152
  )
117
- has_any_llm_event = any(isinstance(e, LLMConvertibleEvent) for e in events)
118
-
119
153
  # Log state for debugging initialization order issues
120
154
  logger.debug(
121
155
  f"init_state called: conversation_id={state.id}, "
122
- f"event_count={len(events)}, "
156
+ f"event_count={event_count}, "
123
157
  f"has_system_prompt={has_system_prompt}, "
124
- f"has_user_message={has_user_message}, "
125
- f"has_any_llm_event={has_any_llm_event}"
158
+ f"has_user_message={has_user_message}"
126
159
  )
127
160
 
128
161
  if has_system_prompt:
129
- # SystemPromptEvent already exists - this is unexpected during normal flow
130
- # but could happen in persistence/resume scenarios
131
- logger.warning(
132
- f"init_state called but SystemPromptEvent already exists. "
133
- f"conversation_id={state.id}, event_count={len(events)}. "
134
- f"This may indicate double initialization or a resume scenario."
162
+ # Restoring/resuming conversations is normal: a system prompt already
163
+ # present means this conversation was initialized previously.
164
+ logger.debug(
165
+ "init_state: SystemPromptEvent already present; skipping init. "
166
+ f"conversation_id={state.id}, event_count={event_count}."
135
167
  )
136
168
  return
137
169
 
138
- # Assert: If there are user messages but no system prompt, something is wrong
139
- # The system prompt should always be added before any user messages
170
+ # Assert: A user message should never appear before the system prompt.
171
+ #
172
+ # NOTE: This is a best-effort check based on the first few events only.
173
+ # Remote conversations can include a ConversationStateUpdateEvent near the
174
+ # start, so we scan a small prefix window.
140
175
  if has_user_message:
141
- event_types = [type(e).__name__ for e in events]
176
+ event_types = [type(e).__name__ for e in prefix_events]
142
177
  logger.error(
143
- f"init_state: User message exists without SystemPromptEvent! "
144
- f"conversation_id={state.id}, events={event_types}"
178
+ f"init_state: User message found in prefix before SystemPromptEvent! "
179
+ f"conversation_id={state.id}, prefix_events={event_types}"
145
180
  )
146
- assert not has_user_message, (
147
- f"Unexpected state: User message exists before SystemPromptEvent. "
148
- f"conversation_id={state.id}, event_count={len(events)}, "
149
- f"event_types={event_types}. "
150
- f"This indicates an initialization order bug - init_state should be "
151
- f"called before any user messages are added to the conversation."
181
+ raise AssertionError(
182
+ "Unexpected state: user message exists before SystemPromptEvent. "
183
+ f"conversation_id={state.id}, event_count={event_count}, "
184
+ f"prefix_event_types={event_types}."
152
185
  )
153
186
 
154
187
  # Prepare system message
@@ -503,5 +503,5 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
503
503
  RuntimeError: If the agent has not been initialized.
504
504
  """
505
505
  if not self._initialized:
506
- raise RuntimeError("Agent not initialized; call initialize() before use")
506
+ raise RuntimeError("Agent not initialized; call _initialize() before use")
507
507
  return self._tools
@@ -103,6 +103,23 @@ class RollingCondenser(PipelinableCondenserBase, ABC):
103
103
  `View` to be passed to the LLM.
104
104
  """
105
105
 
106
+ def hard_context_reset(
107
+ self,
108
+ view: View, # noqa: ARG002
109
+ agent_llm: LLM | None = None, # noqa: ARG002
110
+ ) -> Condensation | None:
111
+ """Perform a hard context reset, if supported by the condenser.
112
+
113
+ By default, rolling condensers do not support hard context resets. Override this
114
+ method to implement hard context reset logic by returning a `Condensation`
115
+ object.
116
+
117
+ This method is invoked when:
118
+ - A HARD condensation requirement is triggered (e.g., by user request)
119
+ - But the condenser raises a NoCondensationAvailableException error
120
+ """
121
+ return None
122
+
106
123
  @abstractmethod
107
124
  def condensation_requirement(
108
125
  self, view: View, agent_llm: LLM | None = None
@@ -142,9 +159,25 @@ class RollingCondenser(PipelinableCondenserBase, ABC):
142
159
  # we do so immediately.
143
160
  return view
144
161
 
145
- # Otherwise re-raise the exception.
146
- else:
147
- raise e
162
+ elif request == CondensationRequirement.HARD:
163
+ # The agent has found itself in a situation where it cannot proceed
164
+ # without condensation, but the condenser cannot provide one. We'll
165
+ # try to recover from this situation by performing a hard context
166
+ # reset, if supported by the condenser.
167
+ try:
168
+ hard_reset_condensation = self.hard_context_reset(
169
+ view, agent_llm=agent_llm
170
+ )
171
+ if hard_reset_condensation is not None:
172
+ return hard_reset_condensation
173
+
174
+ # And if something goes wrong with the hard reset make sure we keep
175
+ # both errors in the stack
176
+ except Exception as hard_reset_exception:
177
+ raise hard_reset_exception from e
178
+
179
+ # In all other situations re-raise the exception.
180
+ raise e
148
181
 
149
182
  # Otherwise we're safe to just return the view.
150
183
  else:
@@ -18,7 +18,12 @@ from openhands.sdk.context.view import View
18
18
  from openhands.sdk.event.base import LLMConvertibleEvent
19
19
  from openhands.sdk.event.condenser import Condensation
20
20
  from openhands.sdk.llm import LLM, Message, TextContent
21
+ from openhands.sdk.logger import get_logger
21
22
  from openhands.sdk.observability.laminar import observe
23
+ from openhands.sdk.utils import maybe_truncate
24
+
25
+
26
+ logger = get_logger(__name__)
22
27
 
23
28
 
24
29
  class Reason(Enum):
@@ -47,6 +52,14 @@ class LLMSummarizingCondenser(RollingCondenser):
47
52
  `keep_first` events in the conversation will never be condensed or summarized.
48
53
  """
49
54
 
55
+ hard_context_reset_max_retries: int = Field(default=5, gt=0)
56
+ """Number of attempts to perform hard context reset before raising an error."""
57
+
58
+ hard_context_reset_context_scaling: float = Field(default=0.8, gt=0.0, lt=1.0)
59
+ """When performing hard context reset, if the summarization fails, reduce the max
60
+ size of each event string by this factor and retry.
61
+ """
62
+
50
63
  @model_validator(mode="after")
51
64
  def validate_keep_first_vs_max_size(self):
52
65
  events_from_tail = self.max_size // 2 - self.keep_first - 1
@@ -120,6 +133,7 @@ class LLMSummarizingCondenser(RollingCondenser):
120
133
  self,
121
134
  forgotten_events: Sequence[LLMConvertibleEvent],
122
135
  summary_offset: int,
136
+ max_event_str_length: int | None = None,
123
137
  ) -> Condensation:
124
138
  """Generate a condensation by using the condenser's LLM to summarize forgotten
125
139
  events.
@@ -127,6 +141,8 @@ class LLMSummarizingCondenser(RollingCondenser):
127
141
  Args:
128
142
  forgotten_events: The list of events to be summarized.
129
143
  summary_offset: The index where the summary event should be inserted.
144
+ max_event_str_length: Optional maximum length for each event string. If
145
+ provided, event strings longer than this will be truncated.
130
146
 
131
147
  Returns:
132
148
  Condensation: The generated condensation object.
@@ -137,7 +153,10 @@ class LLMSummarizingCondenser(RollingCondenser):
137
153
  assert len(forgotten_events) > 0, "No events to condense."
138
154
 
139
155
  # Convert events to strings for the template
140
- event_strings = [str(forgotten_event) for forgotten_event in forgotten_events]
156
+ event_strings = [
157
+ maybe_truncate(str(forgotten_event), truncate_after=max_event_str_length)
158
+ for forgotten_event in forgotten_events
159
+ ]
141
160
 
142
161
  prompt = render_template(
143
162
  os.path.join(os.path.dirname(__file__), "prompts"),
@@ -232,6 +251,51 @@ class LLMSummarizingCondenser(RollingCondenser):
232
251
  # Summary offset is the same as forgetting_start
233
252
  return forgotten_events, forgetting_start
234
253
 
254
+ @observe(ignore_inputs=["view", "agent_llm"])
255
+ def hard_context_reset(
256
+ self,
257
+ view: View,
258
+ agent_llm: LLM | None = None, # noqa: ARG002
259
+ ) -> Condensation | None:
260
+ """Perform a hard context reset by summarizing all events in the view.
261
+
262
+ Depending on how the hard context reset is triggered, this may fail (e.g., if
263
+ the view is too large for the summarizing LLM to handle). In that case, we keep
264
+ trimming down the contents until a summary can be generated.
265
+ """
266
+ max_event_str_length: int | None = None
267
+ attempts_remaining: int = self.hard_context_reset_max_retries
268
+
269
+ while attempts_remaining > 0:
270
+ try:
271
+ return self._generate_condensation(
272
+ forgotten_events=view.events,
273
+ summary_offset=0,
274
+ max_event_str_length=max_event_str_length,
275
+ )
276
+ except Exception as e:
277
+ # If we haven't set a max_event_str_length yet, set it as the largest
278
+ # event string length.
279
+ if max_event_str_length is None:
280
+ max_event_str_length = max(len(str(event)) for event in view.events)
281
+
282
+ # Since the summarization failed, reduce the max_event_str_length by 20%
283
+ assert max_event_str_length is not None
284
+ max_event_str_length = int(
285
+ max_event_str_length * self.hard_context_reset_context_scaling
286
+ )
287
+
288
+ # Log the exception so we can track these failures
289
+ logger.warning(
290
+ f"Hard context reset summarization failed with exception: {e}. "
291
+ f"Reducing max event size to {max_event_str_length} and retrying."
292
+ )
293
+
294
+ attempts_remaining -= 1
295
+
296
+ logger.error("Hard context reset summarization failed after multiple attempts.")
297
+ return None
298
+
235
299
  @observe(ignore_inputs=["view", "agent_llm"])
236
300
  def get_condensation(
237
301
  self, view: View, agent_llm: LLM | None = None
@@ -27,9 +27,10 @@ You can also directly look up a skill's full content by reading its location pat
27
27
  <CUSTOM_SECRETS>
28
28
  ### Credential Access
29
29
  * 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.
30
- * How to use secrets: Simply reference the secret key in your command (e.g., `echo ${GITHUB_TOKEN:0:8}` or `curl -H "Authorization: Bearer $API_KEY" https://api.example.com`). The system will detect the key name in your command text and export it as environment variable before it executes your command.
30
+ * How to use secrets: Simply reference the secret key in your command (e.g., `curl -H "Authorization: Bearer $API_KEY" https://api.example.com`). The system will detect the key name in your command text and export it as environment variable before it executes your command.
31
31
  * Secret detection: The system performs case-insensitive matching to find secret keys in your command text. If a registered secret key appears anywhere in your command, its value will be made available as an environment variable.
32
32
  * Security: Secret values are automatically masked in command output to prevent accidental exposure. You will see `<secret-hidden>` instead of the actual secret value in the output.
33
+ * Avoid exposing raw secrets: Never echo or print the full value of secrets (e.g., avoid `echo $SECRET`). The conversation history may be logged or shared, and exposing raw secret values could compromise security. Instead, use secrets directly in commands where they serve their intended purpose (e.g., in curl headers or git URLs).
33
34
  * Refreshing expired secrets: Some secrets (like GITHUB_TOKEN) may be updated periodically or expire over time. If a secret stops working (e.g., authentication failures), try using it again in a new command - the system should automatically use the refreshed value. For example, if GITHUB_TOKEN was used in a git remote URL and later expired, you can update the remote URL with the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git` to pick up the refreshed token value.
34
35
  * If it still fails, report it to the user.
35
36
 
@@ -27,15 +27,10 @@ from openhands.sdk.context.skills.utils import (
27
27
  validate_skill_name,
28
28
  )
29
29
  from openhands.sdk.logger import get_logger
30
- from openhands.sdk.utils import maybe_truncate
31
30
 
32
31
 
33
32
  logger = get_logger(__name__)
34
33
 
35
- # Maximum characters for third-party skill files (e.g., AGENTS.md, CLAUDE.md, GEMINI.md)
36
- # These files are always active, so we want to keep them reasonably sized
37
- THIRD_PARTY_SKILL_MAX_CHARS = 10_000
38
-
39
34
 
40
35
  class SkillInfo(BaseModel):
41
36
  """Lightweight representation of a skill's essential information.
@@ -485,32 +480,14 @@ class Skill(BaseModel):
485
480
  """Handle third-party skill files (e.g., .cursorrules, AGENTS.md).
486
481
 
487
482
  Creates a Skill with None trigger (always active) if the file type
488
- is recognized. Truncates content if it exceeds the limit.
483
+ is recognized.
489
484
  """
490
485
  skill_name = cls.PATH_TO_THIRD_PARTY_SKILL_NAME.get(path.name.lower())
491
486
 
492
487
  if skill_name is not None:
493
- truncated_content = maybe_truncate(
494
- file_content,
495
- truncate_after=THIRD_PARTY_SKILL_MAX_CHARS,
496
- truncate_notice=(
497
- f"\n\n<TRUNCATED><NOTE>The file {path} exceeded the "
498
- f"maximum length ({THIRD_PARTY_SKILL_MAX_CHARS} "
499
- f"characters) and has been truncated. Only the "
500
- f"beginning and end are shown. You can read the full "
501
- f"file if needed.</NOTE>\n\n"
502
- ),
503
- )
504
-
505
- if len(file_content) > THIRD_PARTY_SKILL_MAX_CHARS:
506
- logger.warning(
507
- f"Third-party skill file {path} ({len(file_content)} chars) "
508
- f"exceeded limit ({THIRD_PARTY_SKILL_MAX_CHARS} chars), truncating"
509
- )
510
-
511
488
  return Skill(
512
489
  name=skill_name,
513
- content=truncated_content,
490
+ content=file_content,
514
491
  source=str(path),
515
492
  trigger=None,
516
493
  )
@@ -74,6 +74,7 @@ class Conversation:
74
74
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
75
75
  ) = DefaultConversationVisualizer,
76
76
  secrets: dict[str, SecretValue] | dict[str, str] | None = None,
77
+ delete_on_close: bool = False,
77
78
  ) -> "LocalConversation": ...
78
79
 
79
80
  @overload
@@ -96,6 +97,7 @@ class Conversation:
96
97
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
97
98
  ) = DefaultConversationVisualizer,
98
99
  secrets: dict[str, SecretValue] | dict[str, str] | None = None,
100
+ delete_on_close: bool = False,
99
101
  ) -> "RemoteConversation": ...
100
102
 
101
103
  def __new__(
@@ -118,6 +120,7 @@ class Conversation:
118
120
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
119
121
  ) = DefaultConversationVisualizer,
120
122
  secrets: dict[str, SecretValue] | dict[str, str] | None = None,
123
+ delete_on_close: bool = False,
121
124
  ) -> BaseConversation:
122
125
  from openhands.sdk.conversation.impl.local_conversation import LocalConversation
123
126
  from openhands.sdk.conversation.impl.remote_conversation import (
@@ -143,6 +146,7 @@ class Conversation:
143
146
  visualizer=visualizer,
144
147
  workspace=workspace,
145
148
  secrets=secrets,
149
+ delete_on_close=delete_on_close,
146
150
  )
147
151
 
148
152
  return LocalConversation(
@@ -159,4 +163,5 @@ class Conversation:
159
163
  workspace=workspace,
160
164
  persistence_dir=persistence_dir,
161
165
  secrets=secrets,
166
+ delete_on_close=delete_on_close,
162
167
  )
@@ -65,6 +65,7 @@ class LocalConversation(BaseConversation):
65
65
  llm_registry: LLMRegistry
66
66
  _cleanup_initiated: bool
67
67
  _hook_processor: HookEventProcessor | None
68
+ delete_on_close: bool = True
68
69
  # Plugin lazy loading state
69
70
  _plugin_specs: list[PluginSource] | None
70
71
  _resolved_plugins: list[ResolvedPluginSource] | None
@@ -90,6 +91,7 @@ class LocalConversation(BaseConversation):
90
91
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
91
92
  ) = DefaultConversationVisualizer,
92
93
  secrets: Mapping[str, SecretValue] | None = None,
94
+ delete_on_close: bool = True,
93
95
  cipher: Cipher | None = None,
94
96
  **_: object,
95
97
  ):
@@ -242,6 +244,7 @@ class LocalConversation(BaseConversation):
242
244
 
243
245
  atexit.register(self.close)
244
246
  self._start_observability_span(str(desired_id))
247
+ self.delete_on_close = delete_on_close
245
248
 
246
249
  @property
247
250
  def id(self) -> ConversationID:
@@ -708,20 +711,23 @@ class LocalConversation(BaseConversation):
708
711
  except AttributeError:
709
712
  # Object may be partially constructed; span fields may be missing.
710
713
  pass
711
- try:
712
- tools_map = self.agent.tools_map
713
- except (AttributeError, RuntimeError):
714
- # Agent not initialized or partially constructed
715
- return
716
- for tool in tools_map.values():
714
+ if self.delete_on_close:
717
715
  try:
718
- executable_tool = tool.as_executable()
719
- executable_tool.executor.close()
720
- except NotImplementedError:
721
- # Tool has no executor, skip it without erroring
722
- continue
723
- except Exception as e:
724
- logger.warning(f"Error closing executor for tool '{tool.name}': {e}")
716
+ tools_map = self.agent.tools_map
717
+ except (AttributeError, RuntimeError):
718
+ # Agent not initialized or partially constructed
719
+ return
720
+ for tool in tools_map.values():
721
+ try:
722
+ executable_tool = tool.as_executable()
723
+ executable_tool.executor.close()
724
+ except NotImplementedError:
725
+ # Tool has no executor, skip it without erroring
726
+ continue
727
+ except Exception as e:
728
+ logger.warning(
729
+ f"Error closing executor for tool '{tool.name}': {e}"
730
+ )
725
731
 
726
732
  def ask_agent(self, question: str) -> str:
727
733
  """Ask the agent a simple, stateless question and get a direct LLM response.
@@ -555,6 +555,7 @@ class RemoteConversation(BaseConversation):
555
555
  _client: httpx.Client
556
556
  _hook_processor: HookEventProcessor | None
557
557
  _cleanup_initiated: bool
558
+ delete_on_close: bool = False
558
559
 
559
560
  def __init__(
560
561
  self,
@@ -573,6 +574,7 @@ class RemoteConversation(BaseConversation):
573
574
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
574
575
  ) = DefaultConversationVisualizer,
575
576
  secrets: Mapping[str, SecretValue] | None = None,
577
+ delete_on_close: bool = False,
576
578
  **_: object,
577
579
  ) -> None:
578
580
  """Remote conversation proxy that talks to an agent server.
@@ -765,6 +767,7 @@ class RemoteConversation(BaseConversation):
765
767
  )
766
768
  self._hook_processor = HookEventProcessor(hook_manager=hook_manager)
767
769
  self._hook_processor.run_session_start()
770
+ self.delete_on_close = delete_on_close
768
771
 
769
772
  def _create_llm_completion_log_callback(self) -> ConversationCallbackType:
770
773
  """Create a callback that writes LLM completion logs to client filesystem."""
@@ -1134,6 +1137,13 @@ class RemoteConversation(BaseConversation):
1134
1137
  pass
1135
1138
 
1136
1139
  self._end_observability_span()
1140
+ if self.delete_on_close:
1141
+ try:
1142
+ # trigger server-side delete_conversation to release resources
1143
+ # like tmux sessions
1144
+ _send_request(self._client, "DELETE", f"/api/conversations/{self.id}")
1145
+ except Exception:
1146
+ pass
1137
1147
 
1138
1148
  def __del__(self) -> None:
1139
1149
  try:
@@ -15,6 +15,12 @@ from openhands.sdk.logger import get_logger
15
15
  logger = get_logger(__name__)
16
16
 
17
17
 
18
+ # Maximum recent events to scan for stuck detection.
19
+ # This window should be large enough to capture repetitive patterns
20
+ # (4 repeats × 2 events per cycle = 8 events minimum, plus buffer for user messages)
21
+ MAX_EVENTS_TO_SCAN_FOR_STUCK_DETECTION: int = 20
22
+
23
+
18
24
  class StuckDetector:
19
25
  """Detects when an agent is stuck in repetitive or unproductive patterns.
20
26
 
@@ -54,8 +60,14 @@ class StuckDetector:
54
60
  return self.thresholds.alternating_pattern
55
61
 
56
62
  def is_stuck(self) -> bool:
57
- """Check if the agent is currently stuck."""
58
- events = list(self.state.events)
63
+ """Check if the agent is currently stuck.
64
+
65
+ Note: To avoid materializing potentially large file-backed event histories,
66
+ only the last MAX_EVENTS_TO_SCAN_FOR_STUCK_DETECTION events are analyzed.
67
+ If a user message exists within this window, only events after it are checked.
68
+ Otherwise, all events in the window are analyzed.
69
+ """
70
+ events = list(self.state.events[-MAX_EVENTS_TO_SCAN_FOR_STUCK_DETECTION:])
59
71
 
60
72
  # Only look at history after the last user message
61
73
  last_user_msg_index = next(
@@ -66,11 +78,8 @@ class StuckDetector:
66
78
  ),
67
79
  -1, # Default to -1 if no user message found
68
80
  )
69
- if last_user_msg_index == -1:
70
- logger.warning("No user message found in history, skipping stuck detection")
71
- return False
72
-
73
- events = events[last_user_msg_index + 1 :]
81
+ if last_user_msg_index != -1:
82
+ events = events[last_user_msg_index + 1 :]
74
83
 
75
84
  # Determine minimum events needed
76
85
  min_threshold = min(
@@ -253,10 +262,10 @@ class StuckDetector:
253
262
  return False
254
263
 
255
264
  def _is_stuck_context_window_error(self, _events: list[Event]) -> bool:
256
- """Detects if we're stuck in a loop of context window errors.
265
+ """Detects if we are stuck in a loop of context window errors.
257
266
 
258
267
  This happens when we repeatedly get context window errors and try to trim,
259
- but the trimming doesn't work, causing us to get more context window errors.
268
+ but the trimming does not work, causing us to get more context window errors.
260
269
  The pattern is repeated AgentCondensationObservation events without any other
261
270
  events between them.
262
271
  """
@@ -1,3 +1,9 @@
1
+ from openhands.sdk.llm.auth import (
2
+ OPENAI_CODEX_MODELS,
3
+ CredentialStore,
4
+ OAuthCredentials,
5
+ OpenAISubscriptionAuth,
6
+ )
1
7
  from openhands.sdk.llm.llm import LLM
2
8
  from openhands.sdk.llm.llm_registry import LLMRegistry, RegistryEvent
3
9
  from openhands.sdk.llm.llm_response import LLMResponse
@@ -22,11 +28,18 @@ from openhands.sdk.llm.utils.verified_models import VERIFIED_MODELS
22
28
 
23
29
 
24
30
  __all__ = [
31
+ # Auth
32
+ "CredentialStore",
33
+ "OAuthCredentials",
34
+ "OpenAISubscriptionAuth",
35
+ "OPENAI_CODEX_MODELS",
36
+ # Core
25
37
  "LLMResponse",
26
38
  "LLM",
27
39
  "LLMRegistry",
28
40
  "RouterLLM",
29
41
  "RegistryEvent",
42
+ # Messages
30
43
  "Message",
31
44
  "MessageToolCall",
32
45
  "TextContent",
@@ -35,10 +48,13 @@ __all__ = [
35
48
  "RedactedThinkingBlock",
36
49
  "ReasoningItemModel",
37
50
  "content_to_str",
51
+ # Streaming
38
52
  "LLMStreamChunk",
39
53
  "TokenCallbackType",
54
+ # Metrics
40
55
  "Metrics",
41
56
  "MetricsSnapshot",
57
+ # Models
42
58
  "VERIFIED_MODELS",
43
59
  "UNVERIFIED_MODELS_EXCLUDING_BEDROCK",
44
60
  "get_unverified_models",
@@ -0,0 +1,28 @@
1
+ """Authentication module for LLM subscription-based access.
2
+
3
+ This module provides OAuth-based authentication for LLM providers that support
4
+ subscription-based access (e.g., ChatGPT Plus/Pro for OpenAI Codex models).
5
+ """
6
+
7
+ from openhands.sdk.llm.auth.credentials import (
8
+ CredentialStore,
9
+ OAuthCredentials,
10
+ )
11
+ from openhands.sdk.llm.auth.openai import (
12
+ OPENAI_CODEX_MODELS,
13
+ OpenAISubscriptionAuth,
14
+ SupportedVendor,
15
+ inject_system_prefix,
16
+ transform_for_subscription,
17
+ )
18
+
19
+
20
+ __all__ = [
21
+ "CredentialStore",
22
+ "OAuthCredentials",
23
+ "OpenAISubscriptionAuth",
24
+ "OPENAI_CODEX_MODELS",
25
+ "SupportedVendor",
26
+ "inject_system_prefix",
27
+ "transform_for_subscription",
28
+ ]