openhands-sdk 1.8.1__tar.gz → 1.9.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 (201) hide show
  1. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/PKG-INFO +6 -1
  2. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/agent.py +64 -0
  3. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/base.py +29 -10
  4. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -0
  5. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +7 -5
  6. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/skill.py +59 -1
  7. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/utils.py +6 -65
  8. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/view.py +6 -11
  9. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/base.py +5 -0
  10. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/event_store.py +84 -12
  11. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/local_conversation.py +7 -0
  12. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/remote_conversation.py +16 -3
  13. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/state.py +25 -2
  14. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/base.py +23 -0
  15. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/__init__.py +4 -1
  16. openhands_sdk-1.9.0/openhands/sdk/critic/base.py +35 -0
  17. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/__init__.py +2 -0
  18. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/agent_finished.py +9 -5
  19. openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/__init__.py +18 -0
  20. openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/chat_template.py +232 -0
  21. openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/client.py +313 -0
  22. openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/critic.py +90 -0
  23. openhands_sdk-1.9.0/openhands/sdk/critic/impl/api/taxonomy.py +180 -0
  24. openhands_sdk-1.9.0/openhands/sdk/critic/result.py +148 -0
  25. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/conversation_error.py +12 -0
  26. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/action.py +10 -0
  27. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/message.py +10 -0
  28. openhands_sdk-1.9.0/openhands/sdk/git/cached_repo.py +459 -0
  29. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/utils.py +118 -3
  30. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/__init__.py +7 -1
  31. openhands_sdk-1.9.0/openhands/sdk/hooks/config.py +289 -0
  32. openhands_sdk-1.9.0/openhands/sdk/io/base.py +100 -0
  33. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/local.py +25 -0
  34. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/memory.py +34 -1
  35. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm.py +6 -2
  36. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_features.py +3 -0
  37. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/telemetry.py +41 -2
  38. openhands_sdk-1.9.0/openhands/sdk/plugin/__init__.py +39 -0
  39. openhands_sdk-1.9.0/openhands/sdk/plugin/fetch.py +231 -0
  40. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/plugin/plugin.py +61 -4
  41. openhands_sdk-1.9.0/openhands/sdk/plugin/types.py +619 -0
  42. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/secret/secrets.py +19 -4
  43. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/PKG-INFO +6 -1
  44. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/SOURCES.txt +16 -0
  45. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/requires.txt +1 -0
  46. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/pyproject.toml +9 -2
  47. openhands_sdk-1.8.1/openhands/sdk/critic/base.py +0 -38
  48. openhands_sdk-1.8.1/openhands/sdk/hooks/config.py +0 -180
  49. openhands_sdk-1.8.1/openhands/sdk/io/base.py +0 -48
  50. openhands_sdk-1.8.1/openhands/sdk/plugin/__init__.py +0 -22
  51. openhands_sdk-1.8.1/openhands/sdk/plugin/types.py +0 -226
  52. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/__init__.py +0 -0
  53. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/__init__.py +0 -0
  54. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  55. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  56. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  57. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  58. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
  59. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  60. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  61. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  62. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  63. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  64. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  65. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  66. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  67. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/agent/utils.py +0 -0
  68. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/__init__.py +0 -0
  69. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/agent_context.py +0 -0
  70. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  71. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/base.py +0 -0
  72. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  73. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  74. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  75. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/condenser/utils.py +0 -0
  76. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  77. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  78. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  79. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  80. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
  81. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/__init__.py +0 -0
  82. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/exceptions.py +0 -0
  83. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/trigger.py +0 -0
  84. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/context/skills/types.py +0 -0
  85. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/__init__.py +0 -0
  86. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/conversation.py +0 -0
  87. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  88. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  89. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/exceptions.py +0 -0
  90. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  91. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  92. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  93. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/response_utils.py +0 -0
  94. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  95. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
  96. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  97. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/title_utils.py +0 -0
  98. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/types.py +0 -0
  99. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  100. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  101. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  102. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  103. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/__init__.py +0 -0
  104. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/base.py +0 -0
  105. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/condenser.py +0 -0
  106. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/conversation_state.py +0 -0
  107. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  108. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  109. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  110. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  111. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/token.py +0 -0
  112. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/types.py +0 -0
  113. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/event/user_action.py +0 -0
  114. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/exceptions.py +0 -0
  115. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/git_changes.py +0 -0
  116. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/git_diff.py +0 -0
  117. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/git/models.py +0 -0
  118. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  119. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/executor.py +0 -0
  120. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/manager.py +0 -0
  121. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/hooks/types.py +0 -0
  122. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/__init__.py +0 -0
  123. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/io/cache.py +0 -0
  124. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/__init__.py +0 -0
  125. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  126. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  127. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  128. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  129. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm_registry.py +0 -0
  130. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/llm_response.py +0 -0
  131. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/message.py +0 -0
  132. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  133. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  134. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/__init__.py +0 -0
  135. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  136. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/common.py +0 -0
  137. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  138. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/__init__.py +0 -0
  139. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/base.py +0 -0
  140. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  141. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  142. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/streaming.py +0 -0
  143. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  144. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  145. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  146. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  147. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  148. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
  149. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/__init__.py +0 -0
  150. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/logger.py +0 -0
  151. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/logger/rolling.py +0 -0
  152. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/__init__.py +0 -0
  153. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/client.py +0 -0
  154. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/definition.py +0 -0
  155. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/exceptions.py +0 -0
  156. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/tool.py +0 -0
  157. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/mcp/utils.py +0 -0
  158. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/__init__.py +0 -0
  159. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/laminar.py +0 -0
  160. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/observability/utils.py +0 -0
  161. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/py.typed +0 -0
  162. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/secret/__init__.py +0 -0
  163. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/__init__.py +0 -0
  164. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/analyzer.py +0 -0
  165. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  166. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  167. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/security/risk.py +0 -0
  168. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/__init__.py +0 -0
  169. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  170. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  171. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/builtins/think.py +0 -0
  172. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/registry.py +0 -0
  173. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/schema.py +0 -0
  174. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/spec.py +0 -0
  175. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/tool/tool.py +0 -0
  176. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/__init__.py +0 -0
  177. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/async_executor.py +0 -0
  178. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/async_utils.py +0 -0
  179. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/cipher.py +0 -0
  180. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/command.py +0 -0
  181. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/deprecation.py +0 -0
  182. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/github.py +0 -0
  183. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/json.py +0 -0
  184. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/models.py +0 -0
  185. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/paging.py +0 -0
  186. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
  187. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  188. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/truncate.py +0 -0
  189. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/utils/visualize.py +0 -0
  190. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/__init__.py +0 -0
  191. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/base.py +0 -0
  192. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/local.py +0 -0
  193. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/models.py +0 -0
  194. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  195. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  196. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/base.py +0 -0
  197. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  198. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands/sdk/workspace/workspace.py +0 -0
  199. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  200. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  201. {openhands_sdk-1.8.1 → openhands_sdk-1.9.0}/setup.cfg +0 -0
@@ -1,10 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.8.1
3
+ Version: 1.9.0
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
+ Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
+ Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
7
+ Project-URL: Documentation, https://docs.openhands.dev/sdk
8
+ Project-URL: Bug Tracker, https://github.com/OpenHands/software-agent-sdk/issues
5
9
  Requires-Python: >=3.12
6
10
  Requires-Dist: deprecation>=2.1.0
7
11
  Requires-Dist: fastmcp>=2.11.3
12
+ Requires-Dist: filelock>=3.20.1
8
13
  Requires-Dist: httpx>=0.27.0
9
14
  Requires-Dist: litellm>=1.80.10
10
15
  Requires-Dist: pydantic>=2.12.5
@@ -17,6 +17,7 @@ from openhands.sdk.conversation import (
17
17
  LocalConversation,
18
18
  )
19
19
  from openhands.sdk.conversation.state import ConversationExecutionStatus
20
+ from openhands.sdk.critic.base import CriticResult
20
21
  from openhands.sdk.event import (
21
22
  ActionEvent,
22
23
  AgentErrorEvent,
@@ -120,6 +121,48 @@ class Agent(AgentBase):
120
121
  )
121
122
  on_event(event)
122
123
 
124
+ def _should_evaluate_with_critic(self, action: Action | None) -> bool:
125
+ """Determine if critic should evaluate based on action type and mode."""
126
+ if self.critic is None:
127
+ return False
128
+
129
+ if self.critic.mode == "all_actions":
130
+ return True
131
+
132
+ # For "finish_and_message" mode, only evaluate FinishAction
133
+ # (MessageEvent will be handled separately in step())
134
+ if isinstance(action, FinishAction):
135
+ return True
136
+
137
+ return False
138
+
139
+ def _evaluate_with_critic(
140
+ self, conversation: LocalConversation, event: ActionEvent | MessageEvent
141
+ ) -> CriticResult | None:
142
+ """Run critic evaluation on the current event and history."""
143
+ if self.critic is None:
144
+ return None
145
+
146
+ try:
147
+ # Build event history including the current event
148
+ events = list(conversation.state.events) + [event]
149
+ llm_convertible_events = [
150
+ e for e in events if isinstance(e, LLMConvertibleEvent)
151
+ ]
152
+
153
+ # Evaluate without git_patch for now
154
+ critic_result = self.critic.evaluate(
155
+ events=llm_convertible_events, git_patch=None
156
+ )
157
+ logger.info(
158
+ f"✓ Critic evaluation: score={critic_result.score:.3f}, "
159
+ f"success={critic_result.success}"
160
+ )
161
+ return critic_result
162
+ except Exception as e:
163
+ logger.error(f"✗ Critic evaluation failed: {e}", exc_info=True)
164
+ return None
165
+
123
166
  def _execute_actions(
124
167
  self,
125
168
  conversation: LocalConversation,
@@ -237,6 +280,7 @@ class Agent(AgentBase):
237
280
  for i, tool_call in enumerate(message.tool_calls):
238
281
  action_event = self._get_action_event(
239
282
  tool_call,
283
+ conversation=conversation,
240
284
  llm_response_id=llm_response.id,
241
285
  on_event=on_event,
242
286
  security_analyzer=state.security_analyzer,
@@ -275,6 +319,14 @@ class Agent(AgentBase):
275
319
  llm_message=message,
276
320
  llm_response_id=llm_response.id,
277
321
  )
322
+ # Run critic evaluation if configured for finish_and_message mode
323
+ if self.critic is not None and self.critic.mode == "finish_and_message":
324
+ critic_result = self._evaluate_with_critic(conversation, msg_event)
325
+ if critic_result is not None:
326
+ # Create new event with critic result
327
+ msg_event = msg_event.model_copy(
328
+ update={"critic_result": critic_result}
329
+ )
278
330
  on_event(msg_event)
279
331
 
280
332
  # Emit VLLM token ids if enabled
@@ -389,6 +441,7 @@ class Agent(AgentBase):
389
441
  def _get_action_event(
390
442
  self,
391
443
  tool_call: MessageToolCall,
444
+ conversation: LocalConversation,
392
445
  llm_response_id: str,
393
446
  on_event: ConversationCallbackType,
394
447
  security_analyzer: analyzer.SecurityAnalyzerBase | None = None,
@@ -477,6 +530,7 @@ class Agent(AgentBase):
477
530
  on_event(event)
478
531
  return
479
532
 
533
+ # Create initial action event
480
534
  action_event = ActionEvent(
481
535
  action=action,
482
536
  thought=thought or [],
@@ -490,6 +544,16 @@ class Agent(AgentBase):
490
544
  security_risk=security_risk,
491
545
  summary=summary,
492
546
  )
547
+
548
+ # Run critic evaluation if configured
549
+ if self._should_evaluate_with_critic(action):
550
+ critic_result = self._evaluate_with_critic(conversation, action_event)
551
+ if critic_result is not None:
552
+ # Create new event with critic result
553
+ action_event = action_event.model_copy(
554
+ update={"critic_result": critic_result}
555
+ )
556
+
493
557
  on_event(action_event)
494
558
  return action_event
495
559
 
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import re
3
5
  import sys
@@ -16,6 +18,7 @@ from pydantic import (
16
18
  from openhands.sdk.context.agent_context import AgentContext
17
19
  from openhands.sdk.context.condenser import CondenserBase
18
20
  from openhands.sdk.context.prompts.prompt import render_template
21
+ from openhands.sdk.critic.base import CriticBase
19
22
  from openhands.sdk.llm import LLM
20
23
  from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
21
24
  from openhands.sdk.logger import get_logger
@@ -37,7 +40,6 @@ if TYPE_CHECKING:
37
40
  ConversationTokenCallbackType,
38
41
  )
39
42
 
40
-
41
43
  logger = get_logger(__name__)
42
44
 
43
45
 
@@ -174,6 +176,16 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
174
176
  ],
175
177
  )
176
178
 
179
+ critic: CriticBase | None = Field(
180
+ default=None,
181
+ description=(
182
+ "EXPERIMENTAL: Optional critic to evaluate agent actions and messages "
183
+ "in real-time. API and behavior may change without notice. "
184
+ "May impact performance, especially in 'all_actions' mode."
185
+ ),
186
+ examples=[{"kind": "AgentFinishedCritic"}],
187
+ )
188
+
177
189
  # Runtime materialized tools; private and non-serializable
178
190
  _tools: dict[str, ToolDefinition] = PrivateAttr(default_factory=dict)
179
191
  _initialized: bool = PrivateAttr(default=False)
@@ -226,8 +238,8 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
226
238
 
227
239
  def init_state(
228
240
  self,
229
- state: "ConversationState",
230
- on_event: "ConversationCallbackType", # noqa: ARG002
241
+ state: ConversationState,
242
+ on_event: ConversationCallbackType, # noqa: ARG002
231
243
  ) -> None:
232
244
  """Initialize the empty conversation state to prepare the agent for user
233
245
  messages.
@@ -238,7 +250,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
238
250
  """
239
251
  self._initialize(state)
240
252
 
241
- def _initialize(self, state: "ConversationState"):
253
+ def _initialize(self, state: ConversationState):
242
254
  """Create an AgentBase instance from an AgentSpec."""
243
255
 
244
256
  if self._initialized:
@@ -310,9 +322,9 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
310
322
  @abstractmethod
311
323
  def step(
312
324
  self,
313
- conversation: "LocalConversation",
314
- on_event: "ConversationCallbackType",
315
- on_token: "ConversationTokenCallbackType | None" = None,
325
+ conversation: LocalConversation,
326
+ on_event: ConversationCallbackType,
327
+ on_token: ConversationTokenCallbackType | None = None,
316
328
  ) -> None:
317
329
  """Taking a step in the conversation.
318
330
 
@@ -332,9 +344,9 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
332
344
 
333
345
  def verify(
334
346
  self,
335
- persisted: "AgentBase",
336
- events: "Sequence[Any] | None" = None,
337
- ) -> "AgentBase":
347
+ persisted: AgentBase,
348
+ events: Sequence[Any] | None = None,
349
+ ) -> AgentBase:
338
350
  """Verify that we can resume this agent from persisted state.
339
351
 
340
352
  This PR's goal is to *not* reconcile configuration between persisted and
@@ -384,6 +396,13 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
384
396
  if isinstance(event, ActionEvent) and event.tool_name
385
397
  }
386
398
 
399
+ # Add builtin tool names from include_default_tools
400
+ # These are runtime names like 'finish', 'think'
401
+ for tool_class_name in self.include_default_tools:
402
+ tool_class = BUILT_IN_TOOL_CLASSES.get(tool_class_name)
403
+ if tool_class is not None:
404
+ runtime_names.add(tool_class.name)
405
+
387
406
  # Only require tools that were actually used in history.
388
407
  missing_used_tools = used_tools - runtime_names
389
408
  if missing_used_tools:
@@ -43,6 +43,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
43
43
  * When committing changes, use `git status` to see all modified files, and stage all files necessary for the commit. Use `git commit -a` whenever possible.
44
44
  * Do NOT commit files that typically shouldn't go into version control (e.g., node_modules/, .env files, build directories, cache files, large binaries) unless explicitly instructed by the user.
45
45
  * If unsure about committing certain files, check for the presence of .gitignore files or ask the user for clarification.
46
+ * When running git commands that may produce paged output (e.g., `git diff`, `git log`, `git show`), use `git --no-pager <command>` or set `GIT_PAGER=cat` to prevent the command from getting stuck waiting for interactive input.
46
47
  </VERSION_CONTROL>
47
48
 
48
49
  <PULL_REQUESTS>
@@ -42,7 +42,11 @@ class LLMSummarizingCondenser(RollingCondenser):
42
42
  llm: LLM
43
43
  max_size: int = Field(default=240, gt=0)
44
44
  max_tokens: int | None = None
45
+
45
46
  keep_first: int = Field(default=2, ge=0)
47
+ """Minimum number of events to preserve at the start of the view. The first
48
+ `keep_first` events in the conversation will never be condensed or summarized.
49
+ """
46
50
 
47
51
  @model_validator(mode="after")
48
52
  def validate_keep_first_vs_max_size(self):
@@ -236,13 +240,11 @@ class LLMSummarizingCondenser(RollingCondenser):
236
240
  # Calculate naive forgetting end (without considering atomic boundaries)
237
241
  naive_end = len(view) - events_from_tail
238
242
 
239
- # Find actual forgetting_start: smallest manipulation index > keep_first
240
- forgetting_start = view.find_next_manipulation_index(
241
- self.keep_first, strict=True
242
- )
243
+ # Find actual forgetting_start: smallest manipulation index >= keep_first
244
+ forgetting_start = view.find_next_manipulation_index(self.keep_first)
243
245
 
244
246
  # Find actual forgetting_end: smallest manipulation index >= naive_end
245
- forgetting_end = view.find_next_manipulation_index(naive_end, strict=False)
247
+ forgetting_end = view.find_next_manipulation_index(naive_end)
246
248
 
247
249
  # Extract events to forget using boundary-aware indices
248
250
  forgotten_events = view[forgetting_start:forgetting_end]
@@ -1,7 +1,7 @@
1
1
  import io
2
2
  import re
3
3
  from pathlib import Path
4
- from typing import Annotated, ClassVar, Union
4
+ from typing import Annotated, ClassVar, Literal, Union
5
5
  from xml.sax.saxutils import escape as xml_escape
6
6
 
7
7
  import frontmatter
@@ -37,6 +37,22 @@ logger = get_logger(__name__)
37
37
  THIRD_PARTY_SKILL_MAX_CHARS = 10_000
38
38
 
39
39
 
40
+ class SkillInfo(BaseModel):
41
+ """Lightweight representation of a skill's essential information.
42
+
43
+ This class provides a standardized, serializable format for skill metadata
44
+ that can be used across different components of the system.
45
+ """
46
+
47
+ name: str
48
+ type: Literal["repo", "knowledge", "agentskills"]
49
+ content: str
50
+ triggers: list[str] = Field(default_factory=list)
51
+ source: str | None = None
52
+ description: str | None = None
53
+ is_agentskills_format: bool = False
54
+
55
+
40
56
  class SkillResources(BaseModel):
41
57
  """Resource directories for a skill (AgentSkills standard).
42
58
 
@@ -560,6 +576,48 @@ class Skill(BaseModel):
560
576
  logger.debug(f"This skill requires user input: {variables}")
561
577
  return len(variables) > 0
562
578
 
579
+ def get_skill_type(self) -> Literal["repo", "knowledge", "agentskills"]:
580
+ """Determine the type of this skill.
581
+
582
+ Returns:
583
+ "agentskills" for AgentSkills format, "repo" for always-active skills,
584
+ "knowledge" for trigger-based skills.
585
+ """
586
+ if self.is_agentskills_format:
587
+ return "agentskills"
588
+ elif self.trigger is None:
589
+ return "repo"
590
+ else:
591
+ return "knowledge"
592
+
593
+ def get_triggers(self) -> list[str]:
594
+ """Extract trigger keywords from this skill.
595
+
596
+ Returns:
597
+ List of trigger strings, or empty list if no triggers.
598
+ """
599
+ if isinstance(self.trigger, KeywordTrigger):
600
+ return self.trigger.keywords
601
+ elif isinstance(self.trigger, TaskTrigger):
602
+ return self.trigger.triggers
603
+ return []
604
+
605
+ def to_skill_info(self) -> SkillInfo:
606
+ """Convert this skill to a SkillInfo.
607
+
608
+ Returns:
609
+ SkillInfo containing the skill's essential information.
610
+ """
611
+ return SkillInfo(
612
+ name=self.name,
613
+ type=self.get_skill_type(),
614
+ content=self.content,
615
+ triggers=self.get_triggers(),
616
+ source=self.source,
617
+ description=self.description,
618
+ is_agentskills_format=self.is_agentskills_format,
619
+ )
620
+
563
621
 
564
622
  def load_skills_from_dir(
565
623
  skill_dir: str | Path,
@@ -5,14 +5,13 @@ from __future__ import annotations
5
5
  import json
6
6
  import os
7
7
  import re
8
- import shutil
9
- import subprocess
10
8
  from pathlib import Path
11
9
  from typing import TYPE_CHECKING
12
10
 
13
11
  from fastmcp.mcp_config import MCPConfig
14
12
 
15
13
  from openhands.sdk.context.skills.exceptions import SkillValidationError
14
+ from openhands.sdk.git.cached_repo import try_cached_clone_or_update
16
15
  from openhands.sdk.logger import get_logger
17
16
 
18
17
 
@@ -316,77 +315,19 @@ def update_skills_repository(
316
315
  ) -> Path | None:
317
316
  """Clone or update the local skills repository.
318
317
 
318
+ Uses the shared git caching infrastructure from openhands.sdk.git.cached_repo.
319
+ When updating, performs: fetch -> checkout ref -> reset --hard to origin/ref.
320
+
319
321
  Args:
320
322
  repo_url: URL of the skills repository.
321
- branch: Branch name to use.
323
+ branch: Branch name to checkout and track.
322
324
  cache_dir: Directory where the repository should be cached.
323
325
 
324
326
  Returns:
325
327
  Path to the local repository if successful, None otherwise.
326
328
  """
327
329
  repo_path = cache_dir / "public-skills"
328
-
329
- try:
330
- if repo_path.exists() and (repo_path / ".git").exists():
331
- logger.debug(f"Updating skills repository at {repo_path}")
332
- try:
333
- subprocess.run(
334
- ["git", "fetch", "origin"],
335
- cwd=repo_path,
336
- check=True,
337
- capture_output=True,
338
- timeout=30,
339
- )
340
- subprocess.run(
341
- ["git", "reset", "--hard", f"origin/{branch}"],
342
- cwd=repo_path,
343
- check=True,
344
- capture_output=True,
345
- timeout=10,
346
- )
347
- logger.debug("Skills repository updated successfully")
348
- except subprocess.TimeoutExpired:
349
- logger.warning("Git pull timed out, using existing cached repository")
350
- except subprocess.CalledProcessError as e:
351
- logger.warning(
352
- f"Failed to update repository: {e.stderr.decode()}, "
353
- f"using existing cached version"
354
- )
355
- else:
356
- logger.info(f"Cloning public skills repository from {repo_url}")
357
- if repo_path.exists():
358
- shutil.rmtree(repo_path)
359
-
360
- subprocess.run(
361
- [
362
- "git",
363
- "clone",
364
- "--depth",
365
- "1",
366
- "--branch",
367
- branch,
368
- repo_url,
369
- str(repo_path),
370
- ],
371
- check=True,
372
- capture_output=True,
373
- timeout=60,
374
- )
375
- logger.debug(f"Skills repository cloned to {repo_path}")
376
-
377
- return repo_path
378
-
379
- except subprocess.TimeoutExpired:
380
- logger.warning(f"Git operation timed out for {repo_url}")
381
- return None
382
- except subprocess.CalledProcessError as e:
383
- logger.warning(
384
- f"Failed to clone/update repository {repo_url}: {e.stderr.decode()}"
385
- )
386
- return None
387
- except Exception as e:
388
- logger.warning(f"Error managing skills repository: {str(e)}")
389
- return None
330
+ return try_cached_clone_or_update(repo_url, repo_path, ref=branch, update=True)
390
331
 
391
332
 
392
333
  def discover_skill_resources(skill_dir: Path) -> SkillResources:
@@ -416,27 +416,22 @@ class View(BaseModel):
416
416
  else:
417
417
  return True
418
418
 
419
- def find_next_manipulation_index(self, threshold: int, strict: bool = False) -> int:
420
- """Find the smallest manipulation index greater than (or equal to) a threshold.
419
+ def find_next_manipulation_index(self, threshold: int) -> int:
420
+ """Find the smallest manipulation index greater than or equal to a threshold.
421
421
 
422
422
  This is a helper method for condensation logic that needs to find safe
423
423
  boundaries for forgetting events. Uses the cached manipulation_indices property.
424
424
 
425
425
  Args:
426
426
  threshold: The threshold value to compare against
427
- strict: If True, finds index > threshold. If False, finds index >= threshold
428
427
 
429
428
  Returns:
430
- The smallest manipulation index that satisfies the condition, or the
431
- threshold itself if no such index exists
429
+ The smallest manipulation index >= threshold, or the threshold itself
430
+ if no such index exists
432
431
  """
433
432
  for idx in self.manipulation_indices:
434
- if strict:
435
- if idx > threshold:
436
- return idx
437
- else:
438
- if idx >= threshold:
439
- return idx
433
+ if idx >= threshold:
434
+ return idx
440
435
  return threshold
441
436
 
442
437
  @staticmethod
@@ -162,6 +162,11 @@ class BaseConversation(ABC):
162
162
  """Set the confirmation policy for the conversation."""
163
163
  ...
164
164
 
165
+ @abstractmethod
166
+ def set_security_analyzer(self, analyzer: SecurityAnalyzerBase | None) -> None:
167
+ """Set the security analyzer for the conversation."""
168
+ ...
169
+
165
170
  @property
166
171
  def confirmation_policy_active(self) -> bool:
167
172
  return not isinstance(self.state.confirmation_policy, NeverConfirm)
@@ -16,17 +16,34 @@ from openhands.sdk.logger import get_logger
16
16
 
17
17
  logger = get_logger(__name__)
18
18
 
19
+ LOCK_FILE_NAME = ".eventlog.lock"
20
+ LOCK_TIMEOUT_SECONDS = 30
21
+
19
22
 
20
23
  class EventLog(EventsListBase):
24
+ """Persistent event log with locking for concurrent writes.
25
+
26
+ This class provides thread-safe and process-safe event storage using
27
+ the FileStore's locking mechanism. Events are persisted to disk and
28
+ can be accessed by index or event ID.
29
+
30
+ Note:
31
+ For LocalFileStore, file locking via flock() does NOT work reliably
32
+ on NFS mounts or network filesystems. Users deploying with shared
33
+ storage should use alternative coordination mechanisms.
34
+ """
35
+
21
36
  _fs: FileStore
22
37
  _dir: str
23
38
  _length: int
39
+ _lock_path: str
24
40
 
25
41
  def __init__(self, fs: FileStore, dir_path: str = EVENTS_DIR) -> None:
26
42
  self._fs = fs
27
43
  self._dir = dir_path
28
44
  self._id_to_idx: dict[EventID, int] = {}
29
45
  self._idx_to_id: dict[int, EventID] = {}
46
+ self._lock_path = f"{dir_path}/{LOCK_FILE_NAME}"
30
47
  self._length = self._scan_and_build_index()
31
48
 
32
49
  def get_index(self, event_id: EventID) -> int:
@@ -54,7 +71,6 @@ class EventLog(EventsListBase):
54
71
  if isinstance(idx, slice):
55
72
  start, stop, step = idx.indices(self._length)
56
73
  return [self._get_single_item(i) for i in range(start, stop, step)]
57
- # idx is int-like (SupportsIndex)
58
74
  return self._get_single_item(idx)
59
75
 
60
76
  def _get_single_item(self, idx: SupportsIndex) -> Event:
@@ -75,26 +91,82 @@ class EventLog(EventsListBase):
75
91
  continue
76
92
  evt = Event.model_validate_json(txt)
77
93
  evt_id = evt.id
78
- # only backfill mapping if missing
79
94
  if i not in self._idx_to_id:
80
95
  self._idx_to_id[i] = evt_id
81
96
  self._id_to_idx.setdefault(evt_id, i)
82
97
  yield evt
83
98
 
84
99
  def append(self, event: Event) -> None:
100
+ """Append an event with locking for thread/process safety.
101
+
102
+ Raises:
103
+ TimeoutError: If the lock cannot be acquired within LOCK_TIMEOUT_SECONDS.
104
+ ValueError: If an event with the same ID already exists.
105
+ """
85
106
  evt_id = event.id
86
- # Check for duplicate ID
87
- if evt_id in self._id_to_idx:
88
- existing_idx = self._id_to_idx[evt_id]
89
- raise ValueError(
90
- f"Event with ID '{evt_id}' already exists at index {existing_idx}"
107
+
108
+ try:
109
+ with self._fs.lock(self._lock_path, timeout=LOCK_TIMEOUT_SECONDS):
110
+ # Sync with disk in case another process wrote while we waited
111
+ disk_length = self._count_events_on_disk()
112
+ if disk_length > self._length:
113
+ self._sync_from_disk(disk_length)
114
+
115
+ if evt_id in self._id_to_idx:
116
+ existing_idx = self._id_to_idx[evt_id]
117
+ raise ValueError(
118
+ f"Event with ID '{evt_id}' already exists at index "
119
+ f"{existing_idx}"
120
+ )
121
+
122
+ target_path = self._path(self._length, event_id=evt_id)
123
+ self._fs.write(target_path, event.model_dump_json(exclude_none=True))
124
+ self._idx_to_id[self._length] = evt_id
125
+ self._id_to_idx[evt_id] = self._length
126
+ self._length += 1
127
+ except TimeoutError:
128
+ logger.error(
129
+ f"Failed to acquire EventLog lock within {LOCK_TIMEOUT_SECONDS}s "
130
+ f"for event {evt_id}"
91
131
  )
132
+ raise
92
133
 
93
- path = self._path(self._length, event_id=evt_id)
94
- self._fs.write(path, event.model_dump_json(exclude_none=True))
95
- self._idx_to_id[self._length] = evt_id
96
- self._id_to_idx[evt_id] = self._length
97
- self._length += 1
134
+ def _count_events_on_disk(self) -> int:
135
+ """Count event files on disk."""
136
+ try:
137
+ paths = self._fs.list(self._dir)
138
+ except FileNotFoundError:
139
+ # Directory doesn't exist yet - expected for new event logs
140
+ return 0
141
+ except Exception as e:
142
+ logger.warning("Error listing event directory %s: %s", self._dir, e)
143
+ return 0
144
+ return sum(
145
+ 1
146
+ for p in paths
147
+ if p.rsplit("/", 1)[-1].startswith("event-") and p.endswith(".json")
148
+ )
149
+
150
+ def _sync_from_disk(self, disk_length: int) -> None:
151
+ """Sync state for events written by other processes.
152
+
153
+ Preserves existing index mappings and only scans new events.
154
+ """
155
+ # Preserve existing mappings
156
+ existing_idx_to_id = dict(self._idx_to_id)
157
+
158
+ # Re-scan to pick up new events
159
+ scanned_length = self._scan_and_build_index()
160
+
161
+ # Restore any mappings that were lost (e.g., for non-UUID event IDs)
162
+ for idx, evt_id in existing_idx_to_id.items():
163
+ if idx not in self._idx_to_id:
164
+ self._idx_to_id[idx] = evt_id
165
+ if evt_id not in self._id_to_idx:
166
+ self._id_to_idx[evt_id] = idx
167
+
168
+ # Use the higher of scanned length or disk_length
169
+ self._length = max(scanned_length, disk_length)
98
170
 
99
171
  def __len__(self) -> int:
100
172
  return self._length
@@ -40,6 +40,7 @@ from openhands.sdk.security.analyzer import SecurityAnalyzerBase
40
40
  from openhands.sdk.security.confirmation_policy import (
41
41
  ConfirmationPolicyBase,
42
42
  )
43
+ from openhands.sdk.utils.cipher import Cipher
43
44
  from openhands.sdk.workspace import LocalWorkspace
44
45
 
45
46
 
@@ -77,6 +78,7 @@ class LocalConversation(BaseConversation):
77
78
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
78
79
  ) = DefaultConversationVisualizer,
79
80
  secrets: Mapping[str, SecretValue] | None = None,
81
+ cipher: Cipher | None = None,
80
82
  **_: object,
81
83
  ):
82
84
  """Initialize the conversation.
@@ -105,6 +107,10 @@ class LocalConversation(BaseConversation):
105
107
  a dict with keys: 'action_observation', 'action_error',
106
108
  'monologue', 'alternating_pattern'. Values are integers
107
109
  representing the number of repetitions before triggering.
110
+ cipher: Optional cipher for encrypting/decrypting secrets in persisted
111
+ state. If provided, secrets are encrypted when saving and
112
+ decrypted when loading. If not provided, secrets are redacted
113
+ (lost) on serialization.
108
114
  """
109
115
  super().__init__() # Initialize with span tracking
110
116
  # Mark cleanup as initiated as early as possible to avoid races or partially
@@ -134,6 +140,7 @@ class LocalConversation(BaseConversation):
134
140
  else None,
135
141
  max_iterations=max_iteration_per_run,
136
142
  stuck_detection=stuck_detection,
143
+ cipher=cipher,
137
144
  )
138
145
 
139
146
  # Default callback: persist every event to state