openhands-sdk 1.6.0__tar.gz → 1.7.1__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 (189) hide show
  1. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/PKG-INFO +2 -2
  2. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/__init__.py +9 -1
  3. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/agent.py +35 -12
  4. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/base.py +11 -0
  5. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +1 -2
  6. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
  7. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/utils.py +18 -4
  8. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/__init__.py +2 -0
  9. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/agent_context.py +16 -8
  10. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/base.py +11 -6
  11. openhands_sdk-1.7.1/openhands/sdk/context/condenser/llm_summarizing_condenser.py +238 -0
  12. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/no_op_condenser.py +2 -1
  13. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/pipeline_condenser.py +10 -9
  14. openhands_sdk-1.7.1/openhands/sdk/context/condenser/utils.py +149 -0
  15. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/prompt.py +40 -2
  16. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
  17. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/__init__.py +2 -0
  18. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/skill.py +146 -0
  19. openhands_sdk-1.7.1/openhands/sdk/context/view.py +503 -0
  20. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/conversation.py +19 -0
  21. openhands_sdk-1.7.1/openhands/sdk/conversation/exceptions.py +50 -0
  22. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/local_conversation.py +60 -8
  23. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/remote_conversation.py +137 -3
  24. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/state.py +41 -1
  25. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/stuck_detector.py +81 -45
  26. openhands_sdk-1.7.1/openhands/sdk/conversation/types.py +45 -0
  27. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/system.py +16 -20
  28. openhands_sdk-1.7.1/openhands/sdk/hooks/__init__.py +30 -0
  29. openhands_sdk-1.7.1/openhands/sdk/hooks/config.py +180 -0
  30. openhands_sdk-1.7.1/openhands/sdk/hooks/conversation_hooks.py +227 -0
  31. openhands_sdk-1.7.1/openhands/sdk/hooks/executor.py +155 -0
  32. openhands_sdk-1.7.1/openhands/sdk/hooks/manager.py +170 -0
  33. openhands_sdk-1.7.1/openhands/sdk/hooks/types.py +40 -0
  34. openhands_sdk-1.7.1/openhands/sdk/io/cache.py +85 -0
  35. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/local.py +39 -2
  36. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/message.py +2 -2
  37. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/mixins/fn_call_converter.py +61 -16
  38. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/mixins/non_native_fc.py +5 -1
  39. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_features.py +64 -24
  40. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/verified_models.py +4 -4
  41. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/logger.py +1 -1
  42. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/schema.py +10 -0
  43. openhands_sdk-1.7.1/openhands/sdk/utils/async_executor.py +115 -0
  44. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/models.py +1 -1
  45. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/PKG-INFO +2 -2
  46. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/SOURCES.txt +16 -0
  47. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/requires.txt +1 -1
  48. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/pyproject.toml +2 -2
  49. openhands_sdk-1.6.0/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -89
  50. openhands_sdk-1.6.0/openhands/sdk/context/view.py +0 -243
  51. openhands_sdk-1.6.0/openhands/sdk/conversation/exceptions.py +0 -25
  52. openhands_sdk-1.6.0/openhands/sdk/conversation/types.py +0 -15
  53. openhands_sdk-1.6.0/openhands/sdk/utils/async_executor.py +0 -106
  54. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/__init__.py +0 -0
  55. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  56. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  57. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  58. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  59. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  60. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  61. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  62. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  63. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  64. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  65. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  66. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  67. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/__init__.py +0 -0
  68. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  69. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/__init__.py +0 -0
  70. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  71. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  72. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/exceptions.py +0 -0
  73. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/trigger.py +0 -0
  74. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/context/skills/types.py +0 -0
  75. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/__init__.py +0 -0
  76. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/base.py +0 -0
  77. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/conversation_stats.py +0 -0
  78. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/event_store.py +0 -0
  79. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/events_list_base.py +0 -0
  80. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/fifo_lock.py +0 -0
  81. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/impl/__init__.py +0 -0
  82. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/persistence_const.py +0 -0
  83. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/response_utils.py +0 -0
  84. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/secret_registry.py +0 -0
  85. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/serialization_diff.py +0 -0
  86. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/title_utils.py +0 -0
  87. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  88. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/base.py +0 -0
  89. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/conversation/visualizer/default.py +0 -0
  90. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/__init__.py +0 -0
  91. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/base.py +0 -0
  92. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/__init__.py +0 -0
  93. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  94. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  95. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  96. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/__init__.py +0 -0
  97. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/base.py +0 -0
  98. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/condenser.py +0 -0
  99. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/conversation_error.py +0 -0
  100. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/conversation_state.py +0 -0
  101. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_completion_log.py +0 -0
  102. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  103. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/action.py +0 -0
  104. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/message.py +0 -0
  105. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  106. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/token.py +0 -0
  107. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/types.py +0 -0
  108. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/event/user_action.py +0 -0
  109. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/exceptions.py +0 -0
  110. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/git_changes.py +0 -0
  111. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/git_diff.py +0 -0
  112. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/models.py +0 -0
  113. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/git/utils.py +0 -0
  114. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/__init__.py +0 -0
  115. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/base.py +0 -0
  116. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/io/memory.py +0 -0
  117. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/__init__.py +0 -0
  118. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  119. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  120. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  121. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/exceptions/types.py +0 -0
  122. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm.py +0 -0
  123. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm_registry.py +0 -0
  124. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/llm_response.py +0 -0
  125. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/__init__.py +0 -0
  126. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/chat_options.py +0 -0
  127. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/common.py +0 -0
  128. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/options/responses_options.py +0 -0
  129. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/__init__.py +0 -0
  130. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/base.py +0 -0
  131. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  132. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/router/impl/random.py +0 -0
  133. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/streaming.py +0 -0
  134. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/metrics.py +0 -0
  135. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_info.py +0 -0
  136. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  137. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  138. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/telemetry.py +0 -0
  139. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  140. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/__init__.py +0 -0
  141. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/logger/rolling.py +0 -0
  142. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/__init__.py +0 -0
  143. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/client.py +0 -0
  144. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/definition.py +0 -0
  145. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/exceptions.py +0 -0
  146. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/tool.py +0 -0
  147. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/mcp/utils.py +0 -0
  148. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/__init__.py +0 -0
  149. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/laminar.py +0 -0
  150. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/observability/utils.py +0 -0
  151. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/py.typed +0 -0
  152. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/secret/__init__.py +0 -0
  153. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/secret/secrets.py +0 -0
  154. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/__init__.py +0 -0
  155. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/analyzer.py +0 -0
  156. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/confirmation_policy.py +0 -0
  157. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/llm_analyzer.py +0 -0
  158. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/security/risk.py +0 -0
  159. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/__init__.py +0 -0
  160. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/__init__.py +0 -0
  161. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/finish.py +0 -0
  162. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/builtins/think.py +0 -0
  163. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/registry.py +0 -0
  164. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/spec.py +0 -0
  165. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/tool/tool.py +0 -0
  166. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/__init__.py +0 -0
  167. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/async_utils.py +0 -0
  168. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/cipher.py +0 -0
  169. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/command.py +0 -0
  170. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/deprecation.py +0 -0
  171. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/github.py +0 -0
  172. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/json.py +0 -0
  173. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/paging.py +0 -0
  174. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/pydantic_diff.py +0 -0
  175. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  176. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/truncate.py +0 -0
  177. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/utils/visualize.py +0 -0
  178. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/__init__.py +0 -0
  179. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/base.py +0 -0
  180. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/local.py +0 -0
  181. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/models.py +0 -0
  182. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/__init__.py +0 -0
  183. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  184. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/base.py +0 -0
  185. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  186. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands/sdk/workspace/workspace.py +0 -0
  187. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  188. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/openhands_sdk.egg-info/top_level.txt +0 -0
  189. {openhands_sdk-1.6.0 → openhands_sdk-1.7.1}/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.1
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
  ]
@@ -25,6 +25,7 @@ from openhands.sdk.event import (
25
25
  ObservationEvent,
26
26
  SystemPromptEvent,
27
27
  TokenEvent,
28
+ UserRejectObservation,
28
29
  )
29
30
  from openhands.sdk.event.condenser import Condensation, CondensationRequest
30
31
  from openhands.sdk.llm import (
@@ -109,17 +110,10 @@ class Agent(AgentBase):
109
110
  event = SystemPromptEvent(
110
111
  source="agent",
111
112
  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
- ],
113
+ # Tools are stored as ToolDefinition objects and converted to
114
+ # OpenAI format with security_risk parameter during LLM completion.
115
+ # See make_llm_completion() in agent/utils.py for details.
116
+ tools=list(self.tools_map.values()),
123
117
  )
124
118
  on_event(event)
125
119
 
@@ -151,9 +145,20 @@ class Agent(AgentBase):
151
145
  self._execute_actions(conversation, pending_actions, on_event)
152
146
  return
153
147
 
148
+ # Check if the last user message was blocked by a UserPromptSubmit hook
149
+ # If so, skip processing and mark conversation as finished
150
+ for event in reversed(list(state.events)):
151
+ if isinstance(event, MessageEvent) and event.source == "user":
152
+ reason = state.pop_blocked_message(event.id)
153
+ if reason is not None:
154
+ logger.info(f"User message blocked by hook: {reason}")
155
+ state.execution_status = ConversationExecutionStatus.FINISHED
156
+ return
157
+ break # Only check the most recent user message
158
+
154
159
  # Prepare LLM messages using the utility function
155
160
  _messages_or_condensation = prepare_llm_messages(
156
- state.events, condenser=self.condenser
161
+ state.events, condenser=self.condenser, llm=self.llm
157
162
  )
158
163
 
159
164
  # Process condensation event before agent sampels another action
@@ -469,8 +474,26 @@ class Agent(AgentBase):
469
474
 
470
475
  It will call the tool's executor and update the state & call callback fn
471
476
  with the observation.
477
+
478
+ If the action was blocked by a PreToolUse hook (recorded in
479
+ state.blocked_actions), a UserRejectObservation is emitted instead
480
+ of executing the action.
472
481
  """
473
482
  state = conversation.state
483
+
484
+ # Check if this action was blocked by a PreToolUse hook
485
+ reason = state.pop_blocked_action(action_event.id)
486
+ if reason is not None:
487
+ logger.info(f"Action '{action_event.tool_name}' blocked by hook: {reason}")
488
+ rejection = UserRejectObservation(
489
+ action_id=action_event.id,
490
+ tool_name=action_event.tool_name,
491
+ tool_call_id=action_event.tool_call_id,
492
+ rejection_reason=reason,
493
+ )
494
+ on_event(rejection)
495
+ return rejection
496
+
474
497
  tool = self.tools_map.get(action_event.tool_name, None)
475
498
  if tool is None:
476
499
  raise RuntimeError(
@@ -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
@@ -1,3 +1,2 @@
1
1
  * Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
2
- * ALWAYS send a brief preamble to the user explaining what you're about to do before each tool call, using 8 - 12 words, with a friendly and curious tone.
3
- * You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
2
+ * You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
@@ -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 %}
@@ -117,6 +117,7 @@ def prepare_llm_messages(
117
117
  events: Sequence[Event],
118
118
  condenser: None = None,
119
119
  additional_messages: list[Message] | None = None,
120
+ llm: LLM | None = None,
120
121
  ) -> list[Message]: ...
121
122
 
122
123
 
@@ -125,6 +126,7 @@ def prepare_llm_messages(
125
126
  events: Sequence[Event],
126
127
  condenser: CondenserBase,
127
128
  additional_messages: list[Message] | None = None,
129
+ llm: LLM | None = None,
128
130
  ) -> list[Message] | Condensation: ...
129
131
 
130
132
 
@@ -132,6 +134,7 @@ def prepare_llm_messages(
132
134
  events: Sequence[Event],
133
135
  condenser: CondenserBase | None = None,
134
136
  additional_messages: list[Message] | None = None,
137
+ llm: LLM | None = None,
135
138
  ) -> list[Message] | Condensation:
136
139
  """Prepare LLM messages from conversation context.
137
140
 
@@ -140,13 +143,15 @@ def prepare_llm_messages(
140
143
  It handles condensation internally and calls the callback when needed.
141
144
 
142
145
  Args:
143
- state: The conversation state containing events
146
+ events: Sequence of events to prepare messages from
144
147
  condenser: Optional condenser for handling context window limits
145
148
  additional_messages: Optional additional messages to append
146
- on_event: Optional callback for handling condensation events
149
+ llm: Optional LLM instance from the agent, passed to condenser for
150
+ token counting or other LLM features
147
151
 
148
152
  Returns:
149
- List of messages ready for LLM completion
153
+ List of messages ready for LLM completion, or a Condensation event
154
+ if condensation is needed
150
155
 
151
156
  Raises:
152
157
  RuntimeError: If condensation is needed but no callback is provided
@@ -160,7 +165,7 @@ def prepare_llm_messages(
160
165
  # produce a list of events, exactly as expected, or a
161
166
  # new condensation that needs to be processed
162
167
  if condenser is not None:
163
- condensation_result = condenser.condense(view)
168
+ condensation_result = condenser.condense(view, agent_llm=llm)
164
169
 
165
170
  match condensation_result:
166
171
  case View():
@@ -195,6 +200,15 @@ def make_llm_completion(
195
200
 
196
201
  Returns:
197
202
  LLMResponse from the LLM completion call
203
+
204
+ Note:
205
+ Always exposes a 'security_risk' parameter in tool schemas via
206
+ add_security_risk_prediction=True. This ensures the schema remains
207
+ consistent, even if the security analyzer is disabled. Validation of
208
+ this field happens dynamically at runtime depending on the analyzer
209
+ configured. This allows weaker models to omit risk field and bypass
210
+ validation requirements when analyzer is disabled. For detailed logic,
211
+ see `_extract_security_risk` method in agent.py.
198
212
  """
199
213
  if llm.uses_responses_api():
200
214
  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():
@@ -3,6 +3,7 @@ from logging import getLogger
3
3
 
4
4
  from openhands.sdk.context.view import View
5
5
  from openhands.sdk.event.condenser import Condensation
6
+ from openhands.sdk.llm import LLM
6
7
  from openhands.sdk.utils.models import (
7
8
  DiscriminatedUnionMixin,
8
9
  )
@@ -28,7 +29,7 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
28
29
  """
29
30
 
30
31
  @abstractmethod
31
- def condense(self, view: View) -> View | Condensation:
32
+ def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
32
33
  """Condense a sequence of events into a potentially smaller list.
33
34
 
34
35
  New condenser strategies should override this method to implement their own
@@ -37,6 +38,8 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
37
38
 
38
39
  Args:
39
40
  view: A view of the history containing all events that should be condensed.
41
+ agent_llm: LLM instance used by the agent. Condensers use this for token
42
+ counting purposes. Defaults to None.
40
43
 
41
44
  Returns:
42
45
  View | Condensation: A condensed view of the events or an event indicating
@@ -77,18 +80,20 @@ class RollingCondenser(PipelinableCondenserBase, ABC):
77
80
  """
78
81
 
79
82
  @abstractmethod
80
- def should_condense(self, view: View) -> bool:
83
+ def should_condense(self, view: View, agent_llm: LLM | None = None) -> bool:
81
84
  """Determine if a view should be condensed."""
82
85
 
83
86
  @abstractmethod
84
- def get_condensation(self, view: View) -> Condensation:
87
+ def get_condensation(
88
+ self, view: View, agent_llm: LLM | None = None
89
+ ) -> Condensation:
85
90
  """Get the condensation from a view."""
86
91
 
87
- def condense(self, view: View) -> View | Condensation:
92
+ def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
88
93
  # If we trigger the condenser-specific condensation threshold, compute and
89
94
  # return the condensation.
90
- if self.should_condense(view):
91
- return self.get_condensation(view)
95
+ if self.should_condense(view, agent_llm=agent_llm):
96
+ return self.get_condensation(view, agent_llm=agent_llm)
92
97
 
93
98
  # Otherwise we're safe to just return the view.
94
99
  else:
@@ -0,0 +1,238 @@
1
+ import os
2
+ from collections.abc import Sequence
3
+ from enum import Enum
4
+
5
+ from pydantic import Field, model_validator
6
+
7
+ from openhands.sdk.context.condenser.base import RollingCondenser
8
+ from openhands.sdk.context.condenser.utils import (
9
+ get_suffix_length_for_token_reduction,
10
+ get_total_token_count,
11
+ )
12
+ from openhands.sdk.context.prompts import render_template
13
+ from openhands.sdk.context.view import View
14
+ from openhands.sdk.event.base import LLMConvertibleEvent
15
+ from openhands.sdk.event.condenser import Condensation
16
+ from openhands.sdk.event.llm_convertible import MessageEvent
17
+ from openhands.sdk.llm import LLM, Message, TextContent
18
+ from openhands.sdk.observability.laminar import observe
19
+
20
+
21
+ class Reason(Enum):
22
+ """Reasons for condensation."""
23
+
24
+ REQUEST = "request"
25
+ TOKENS = "tokens"
26
+ EVENTS = "events"
27
+
28
+
29
+ class LLMSummarizingCondenser(RollingCondenser):
30
+ """LLM-based condenser that summarizes forgotten events.
31
+
32
+ Uses an independent LLM (stored in the `llm` attribute) for generating summaries
33
+ of forgotten events. The optional `agent_llm` parameter passed to condense() is
34
+ the LLM used by the agent for token counting purposes, and you should not assume
35
+ it is the same as the one defined in this condenser.
36
+ """
37
+
38
+ llm: LLM
39
+ max_size: int = Field(default=120, gt=0)
40
+ max_tokens: int | None = None
41
+ keep_first: int = Field(default=4, ge=0)
42
+
43
+ @model_validator(mode="after")
44
+ def validate_keep_first_vs_max_size(self):
45
+ events_from_tail = self.max_size // 2 - self.keep_first - 1
46
+ if events_from_tail <= 0:
47
+ raise ValueError(
48
+ "keep_first must be less than max_size // 2 to leave room for "
49
+ "condensation"
50
+ )
51
+ return self
52
+
53
+ def handles_condensation_requests(self) -> bool:
54
+ return True
55
+
56
+ def get_condensation_reasons(
57
+ self, view: View, agent_llm: LLM | None = None
58
+ ) -> set[Reason]:
59
+ """Determine the reasons why the view should be condensed.
60
+
61
+ Args:
62
+ view: The current view to evaluate.
63
+ agent_llm: The LLM used by the agent. Required if token counting is needed.
64
+
65
+ Returns:
66
+ A set of Reason enums indicating why condensation is needed.
67
+ """
68
+ reasons = set()
69
+
70
+ # Reason 1: Unhandled condensation request. The view handles the detection of
71
+ # these requests while processing the event stream.
72
+ if view.unhandled_condensation_request:
73
+ reasons.add(Reason.REQUEST)
74
+
75
+ # Reason 2: Token limit is provided and exceeded.
76
+ if self.max_tokens and agent_llm:
77
+ total_tokens = get_total_token_count(view.events, agent_llm)
78
+ if total_tokens > self.max_tokens:
79
+ reasons.add(Reason.TOKENS)
80
+
81
+ # Reason 3: View exceeds maximum size in number of events.
82
+ if len(view) > self.max_size:
83
+ reasons.add(Reason.EVENTS)
84
+
85
+ return reasons
86
+
87
+ def should_condense(self, view: View, agent_llm: LLM | None = None) -> bool:
88
+ reasons = self.get_condensation_reasons(view, agent_llm)
89
+ return reasons != set()
90
+
91
+ def _get_summary_event_content(self, view: View) -> str:
92
+ """Extract the text content from the summary event in the view, if any.
93
+
94
+ If there is no summary event or it does not contain text content, returns an
95
+ empty string.
96
+ """
97
+ summary_event_content: str = ""
98
+
99
+ summary_event = view.summary_event
100
+ if isinstance(summary_event, MessageEvent):
101
+ message_content = summary_event.llm_message.content[0]
102
+ if isinstance(message_content, TextContent):
103
+ summary_event_content = message_content.text
104
+
105
+ return summary_event_content
106
+
107
+ def _generate_condensation(
108
+ self,
109
+ summary_event_content: str,
110
+ forgotten_events: Sequence[LLMConvertibleEvent],
111
+ summary_offset: int,
112
+ ) -> Condensation:
113
+ """Generate a condensation by using the condenser's LLM to summarize forgotten
114
+ events.
115
+
116
+ Args:
117
+ summary_event_content: The content of the previous summary event.
118
+ forgotten_events: The list of events to be summarized.
119
+ summary_offset: The index where the summary event should be inserted.
120
+
121
+ Returns:
122
+ Condensation: The generated condensation object.
123
+ """
124
+ # Convert events to strings for the template
125
+ event_strings = [str(forgotten_event) for forgotten_event in forgotten_events]
126
+
127
+ prompt = render_template(
128
+ os.path.join(os.path.dirname(__file__), "prompts"),
129
+ "summarizing_prompt.j2",
130
+ previous_summary=summary_event_content,
131
+ events=event_strings,
132
+ )
133
+
134
+ messages = [Message(role="user", content=[TextContent(text=prompt)])]
135
+
136
+ # Do not pass extra_body explicitly. The LLM handles forwarding
137
+ # litellm_extra_body only when it is non-empty.
138
+ llm_response = self.llm.completion(
139
+ messages=messages,
140
+ )
141
+ # Extract summary from the LLMResponse message
142
+ summary = None
143
+ if llm_response.message.content:
144
+ first_content = llm_response.message.content[0]
145
+ if isinstance(first_content, TextContent):
146
+ summary = first_content.text
147
+
148
+ return Condensation(
149
+ forgotten_event_ids=[event.id for event in forgotten_events],
150
+ summary=summary,
151
+ summary_offset=summary_offset,
152
+ llm_response_id=llm_response.id,
153
+ )
154
+
155
+ def _get_forgotten_events(
156
+ self, view: View, agent_llm: LLM | None = None
157
+ ) -> tuple[Sequence[LLMConvertibleEvent], int]:
158
+ """Identify events to be forgotten and the summary offset.
159
+
160
+ Relies on the condensation reasons to determine how many events we need to drop
161
+ in order to maintain our resource constraints. Uses manipulation indices to
162
+ ensure forgetting ranges respect atomic unit boundaries.
163
+
164
+ Args:
165
+ view: The current view from which to identify forgotten events.
166
+ agent_llm: The LLM used by the agent, required for token-based calculations.
167
+
168
+ Returns:
169
+ A tuple of (events to forget, summary_offset).
170
+ """
171
+ reasons = self.get_condensation_reasons(view, agent_llm=agent_llm)
172
+ assert reasons != set(), "No condensation reasons found."
173
+
174
+ suffix_events_to_keep: set[int] = set()
175
+
176
+ if Reason.REQUEST in reasons:
177
+ target_size = len(view) // 2
178
+ suffix_events_to_keep.add(target_size - self.keep_first - 1)
179
+
180
+ if Reason.EVENTS in reasons:
181
+ target_size = self.max_size // 2
182
+ suffix_events_to_keep.add(target_size - self.keep_first - 1)
183
+
184
+ if Reason.TOKENS in reasons:
185
+ # Compute the number of tokens we need to eliminate to be under half the
186
+ # max_tokens value. We know max_tokens and the agent LLM are not None here
187
+ # because we can't have Reason.TOKENS without them.
188
+ assert self.max_tokens is not None
189
+ assert agent_llm is not None
190
+
191
+ total_tokens = get_total_token_count(view.events, agent_llm)
192
+ tokens_to_reduce = total_tokens - (self.max_tokens // 2)
193
+
194
+ suffix_events_to_keep.add(
195
+ get_suffix_length_for_token_reduction(
196
+ events=view.events[self.keep_first :],
197
+ llm=agent_llm,
198
+ token_reduction=tokens_to_reduce,
199
+ )
200
+ )
201
+
202
+ # We might have multiple reasons to condense, so pick the strictest condensation
203
+ # to ensure all resource constraints are met.
204
+ events_from_tail = min(suffix_events_to_keep)
205
+
206
+ # Calculate naive forgetting end (without considering atomic boundaries)
207
+ naive_end = len(view) - events_from_tail
208
+
209
+ # Find actual forgetting_start: smallest manipulation index > keep_first
210
+ forgetting_start = view.find_next_manipulation_index(
211
+ self.keep_first, strict=True
212
+ )
213
+
214
+ # Find actual forgetting_end: smallest manipulation index >= naive_end
215
+ forgetting_end = view.find_next_manipulation_index(naive_end, strict=False)
216
+
217
+ # Extract events to forget using boundary-aware indices
218
+ forgotten_events = view[forgetting_start:forgetting_end]
219
+
220
+ # Summary offset is the same as forgetting_start
221
+ return forgotten_events, forgetting_start
222
+
223
+ @observe(ignore_inputs=["view", "agent_llm"])
224
+ def get_condensation(
225
+ self, view: View, agent_llm: LLM | None = None
226
+ ) -> Condensation:
227
+ # The condensation is dependent on the events we want to drop and the previous
228
+ # summary.
229
+ summary_event_content = self._get_summary_event_content(view)
230
+ forgotten_events, summary_offset = self._get_forgotten_events(
231
+ view, agent_llm=agent_llm
232
+ )
233
+
234
+ return self._generate_condensation(
235
+ summary_event_content=summary_event_content,
236
+ forgotten_events=forgotten_events,
237
+ summary_offset=summary_offset,
238
+ )
@@ -1,6 +1,7 @@
1
1
  from openhands.sdk.context.condenser.base import CondenserBase
2
2
  from openhands.sdk.context.view import View
3
3
  from openhands.sdk.event.condenser import Condensation
4
+ from openhands.sdk.llm import LLM
4
5
 
5
6
 
6
7
  class NoOpCondenser(CondenserBase):
@@ -9,5 +10,5 @@ class NoOpCondenser(CondenserBase):
9
10
  Primarily intended for testing purposes.
10
11
  """
11
12
 
12
- def condense(self, view: View) -> View | Condensation:
13
+ def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation: # noqa: ARG002
13
14
  return view
@@ -1,15 +1,16 @@
1
1
  from openhands.sdk.context.condenser.base import CondenserBase
2
2
  from openhands.sdk.context.view import View
3
3
  from openhands.sdk.event.condenser import Condensation
4
+ from openhands.sdk.llm import LLM
4
5
 
5
6
 
6
7
  class PipelineCondenser(CondenserBase):
7
8
  """A condenser that applies a sequence of condensers in order.
8
9
 
9
10
  All condensers are defined primarily by their `condense` method, which takes a
10
- `View` and returns either a new `View` or a `Condensation` event. That means we can
11
- chain multiple condensers together by passing `View`s along and exiting early if any
12
- condenser returns a `Condensation`.
11
+ `View` and an optional `agent_llm` parameter, returning either a new `View` or a
12
+ `Condensation` event. That means we can chain multiple condensers together by
13
+ passing `View`s along and exiting early if any condenser returns a `Condensation`.
13
14
 
14
15
  For example:
15
16
 
@@ -20,20 +21,20 @@ class PipelineCondenser(CondenserBase):
20
21
  CondenserC(...),
21
22
  ])
22
23
 
23
- result = condenser.condense(view)
24
+ result = condenser.condense(view, agent_llm=agent_llm)
24
25
 
25
26
  # Doing the same thing without the pipeline condenser requires more boilerplate
26
27
  # for the monadic chaining
27
28
  other_result = view
28
29
 
29
30
  if isinstance(other_result, View):
30
- other_result = CondenserA(...).condense(other_result)
31
+ other_result = CondenserA(...).condense(other_result, agent_llm=agent_llm)
31
32
 
32
33
  if isinstance(other_result, View):
33
- other_result = CondenserB(...).condense(other_result)
34
+ other_result = CondenserB(...).condense(other_result, agent_llm=agent_llm)
34
35
 
35
36
  if isinstance(other_result, View):
36
- other_result = CondenserC(...).condense(other_result)
37
+ other_result = CondenserC(...).condense(other_result, agent_llm=agent_llm)
37
38
 
38
39
  assert result == other_result
39
40
  """
@@ -41,12 +42,12 @@ class PipelineCondenser(CondenserBase):
41
42
  condensers: list[CondenserBase]
42
43
  """The list of condensers to apply in order."""
43
44
 
44
- def condense(self, view: View) -> View | Condensation:
45
+ def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
45
46
  result: View | Condensation = view
46
47
  for condenser in self.condensers:
47
48
  if isinstance(result, Condensation):
48
49
  break
49
- result = condenser.condense(result)
50
+ result = condenser.condense(result, agent_llm=agent_llm)
50
51
  return result
51
52
 
52
53
  def handles_condensation_requests(self) -> bool: