openhands-sdk 1.27.0__tar.gz → 1.28.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 (273) hide show
  1. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/acp_agent.py +55 -9
  3. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/agent.py +20 -3
  4. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/utils.py +14 -14
  5. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +15 -4
  6. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/conversation.py +6 -0
  7. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/impl/local_conversation.py +73 -4
  8. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/impl/remote_conversation.py +38 -1
  9. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/request.py +11 -0
  10. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/exceptions/classifier.py +12 -2
  11. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/llm.py +149 -26
  12. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/llm_profile_store.py +4 -0
  13. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/model_features.py +35 -7
  14. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/model_info.py +6 -0
  15. openhands_sdk-1.28.0/openhands/sdk/llm/utils/openhands_provider.py +87 -0
  16. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/telemetry.py +5 -5
  17. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/settings/api_models.py +13 -2
  18. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/settings/model.py +14 -1
  19. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/__init__.py +12 -0
  20. openhands_sdk-1.28.0/openhands/sdk/tool/client_tool.py +404 -0
  21. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  22. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands_sdk.egg-info/SOURCES.txt +4 -0
  23. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/pyproject.toml +1 -1
  24. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/__init__.py +0 -0
  25. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/__init__.py +0 -0
  26. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/acp_models.py +0 -0
  27. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/base.py +0 -0
  28. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  29. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  30. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  31. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  32. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  33. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  34. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
  35. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  36. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  37. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  38. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  39. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/system_prompt.j2 +0 -0
  40. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  41. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  42. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  43. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  44. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  45. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/banner.py +0 -0
  46. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/__init__.py +0 -0
  47. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/agent_context.py +0 -0
  48. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  49. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/base.py +0 -0
  50. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  51. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  52. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  53. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/condenser/utils.py +0 -0
  54. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  55. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  56. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  57. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  58. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
  59. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/skills/__init__.py +0 -0
  60. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/__init__.py +0 -0
  61. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  62. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  63. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/base.py +0 -0
  64. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  65. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  66. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  67. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  68. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/context/view/view.py +0 -0
  69. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/__init__.py +0 -0
  70. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/base.py +0 -0
  71. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/cancellation.py +0 -0
  72. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  73. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/event_store.py +0 -0
  74. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  75. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/exceptions.py +0 -0
  76. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  77. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  78. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  79. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  80. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/response_utils.py +0 -0
  81. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  82. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
  83. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/state.py +0 -0
  84. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  85. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/title_utils.py +0 -0
  86. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/types.py +0 -0
  87. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  88. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  89. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  90. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/__init__.py +0 -0
  91. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/base.py +0 -0
  92. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  93. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  94. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  95. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  96. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  97. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  98. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  99. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  100. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  101. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/critic/result.py +0 -0
  102. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/__init__.py +0 -0
  103. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  104. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/base.py +0 -0
  105. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/condenser.py +0 -0
  106. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/conversation_error.py +0 -0
  107. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/conversation_state.py +0 -0
  108. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/hook_execution.py +0 -0
  109. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  110. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  111. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  112. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  113. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  114. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  115. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  116. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/resume_transcript.py +0 -0
  117. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/streaming_delta.py +0 -0
  118. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/token.py +0 -0
  119. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/types.py +0 -0
  120. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/event/user_action.py +0 -0
  121. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/__init__.py +0 -0
  122. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/fetch.py +0 -0
  123. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  124. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/info.py +0 -0
  125. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  126. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  127. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  128. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  129. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/cached_repo.py +0 -0
  130. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/exceptions.py +0 -0
  131. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/git_changes.py +0 -0
  132. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/git_diff.py +0 -0
  133. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/models.py +0 -0
  134. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/git/utils.py +0 -0
  135. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/__init__.py +0 -0
  136. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/config.py +0 -0
  137. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  138. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/executor.py +0 -0
  139. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/manager.py +0 -0
  140. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/hooks/types.py +0 -0
  141. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/io/__init__.py +0 -0
  142. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/io/base.py +0 -0
  143. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/io/cache.py +0 -0
  144. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/io/local.py +0 -0
  145. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/io/memory.py +0 -0
  146. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/__init__.py +0 -0
  147. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  148. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  149. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/auth/openai.py +0 -0
  150. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  151. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  152. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  153. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  154. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/llm_registry.py +0 -0
  155. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/llm_response.py +0 -0
  156. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/message.py +0 -0
  157. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  158. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  159. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  160. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/options/__init__.py +0 -0
  161. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  162. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/options/common.py +0 -0
  163. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  164. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/router/__init__.py +0 -0
  165. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/router/base.py +0 -0
  166. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  167. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  168. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/streaming.py +0 -0
  169. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  170. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  171. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  172. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  173. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  174. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  175. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  176. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  177. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
  178. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/logger/__init__.py +0 -0
  179. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/logger/logger.py +0 -0
  180. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/logger/rolling.py +0 -0
  181. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/marketplace/__init__.py +0 -0
  182. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/marketplace/types.py +0 -0
  183. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/__init__.py +0 -0
  184. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/client.py +0 -0
  185. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/definition.py +0 -0
  186. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/exceptions.py +0 -0
  187. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/tool.py +0 -0
  188. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/mcp/utils.py +0 -0
  189. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/observability/__init__.py +0 -0
  190. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/observability/laminar.py +0 -0
  191. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/observability/utils.py +0 -0
  192. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/__init__.py +0 -0
  193. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/fetch.py +0 -0
  194. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/installed.py +0 -0
  195. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/loader.py +0 -0
  196. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/plugin.py +0 -0
  197. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/source.py +0 -0
  198. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/plugin/types.py +0 -0
  199. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/py.typed +0 -0
  200. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/secret/__init__.py +0 -0
  201. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/secret/secrets.py +0 -0
  202. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/__init__.py +0 -0
  203. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/analyzer.py +0 -0
  204. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  205. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  206. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  207. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  208. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  209. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/ensemble.py +0 -0
  210. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  211. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  212. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  213. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  214. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/risk.py +0 -0
  215. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/security/shell_parser.py +0 -0
  216. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/settings/__init__.py +0 -0
  217. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/settings/acp_providers.py +0 -0
  218. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/settings/metadata.py +0 -0
  219. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/__init__.py +0 -0
  220. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/exceptions.py +0 -0
  221. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/execute.py +0 -0
  222. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/fetch.py +0 -0
  223. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/installed.py +0 -0
  224. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/skill.py +0 -0
  225. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/trigger.py +0 -0
  226. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/types.py +0 -0
  227. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/skills/utils.py +0 -0
  228. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/subagent/__init__.py +0 -0
  229. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/subagent/load.py +0 -0
  230. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/subagent/registry.py +0 -0
  231. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/subagent/schema.py +0 -0
  232. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/testing/__init__.py +0 -0
  233. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/testing/test_llm.py +0 -0
  234. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  235. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  236. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  237. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  238. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/builtins/think.py +0 -0
  239. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/registry.py +0 -0
  240. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/schema.py +0 -0
  241. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/spec.py +0 -0
  242. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/tool/tool.py +0 -0
  243. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/__init__.py +0 -0
  244. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/async_executor.py +0 -0
  245. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/async_utils.py +0 -0
  246. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/cipher.py +0 -0
  247. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/command.py +0 -0
  248. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/datetime.py +0 -0
  249. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/deprecation.py +0 -0
  250. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/github.py +0 -0
  251. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/json.py +0 -0
  252. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/models.py +0 -0
  253. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/paging.py +0 -0
  254. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/path.py +0 -0
  255. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
  256. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  257. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/redact.py +0 -0
  258. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/truncate.py +0 -0
  259. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/utils/visualize.py +0 -0
  260. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/__init__.py +0 -0
  261. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/base.py +0 -0
  262. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/local.py +0 -0
  263. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/models.py +0 -0
  264. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  265. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  266. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/remote/base.py +0 -0
  267. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  268. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/repo.py +0 -0
  269. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands/sdk/workspace/workspace.py +0 -0
  270. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  271. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands_sdk.egg-info/requires.txt +0 -0
  272. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  273. {openhands_sdk-1.27.0 → openhands_sdk-1.28.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.27.0
3
+ Version: 1.28.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
@@ -509,15 +509,19 @@ async def _maybe_set_session_model(
509
509
  use the protocol call for initial selection (codex-acp, gemini-cli) get a
510
510
  one-shot ``set_session_model`` call here.
511
511
 
512
+ For unknown/custom providers (e.g. Devin CLI), we fall back to the generic
513
+ ``set_config_option`` method with configId="model", which is a standard ACP
514
+ method that many custom ACP servers support.
515
+
512
516
  Runtime, mid-conversation switches go through
513
517
  :meth:`ACPAgent.set_acp_model` instead, which always uses
514
518
  ``set_session_model`` and is gated on the separate
515
519
  ``supports_runtime_model_switch`` capability flag.
516
520
 
517
- Returns ``True`` only when this issued a ``set_session_model`` call — i.e.
521
+ Returns ``True`` only when this issued a model-setting call that succeeded — i.e.
518
522
  the override was actually pushed to the server via *this* path. ``False``
519
523
  when there is nothing to apply (no ``acp_model``) or the provider selects
520
- its model another way (``_meta``) or not at all (unknown/custom server), so
524
+ its model another way (``_meta``) or the server rejected the call, so
521
525
  the caller can tell whether the live session is really running ``acp_model``.
522
526
  """
523
527
  if not acp_model:
@@ -526,6 +530,29 @@ async def _maybe_set_session_model(
526
530
  if provider is not None and provider.supports_set_session_model:
527
531
  await conn.set_session_model(model_id=acp_model, session_id=session_id)
528
532
  return True
533
+ # For unknown/custom providers, try the generic set_config_option method
534
+ # which is a standard ACP protocol method for setting configuration options
535
+ if provider is None:
536
+ try:
537
+ await conn.set_config_option(
538
+ config_id="model",
539
+ value=acp_model,
540
+ session_id=session_id,
541
+ )
542
+ logger.info(
543
+ "Set model %r on unknown/custom ACP server %s via set_config_option",
544
+ acp_model,
545
+ agent_name,
546
+ )
547
+ return True
548
+ except ACPRequestError as e:
549
+ logger.warning(
550
+ "Could not set model %r on unknown/custom ACP server %s via "
551
+ "set_config_option (%s); the session will use the server default",
552
+ acp_model,
553
+ agent_name,
554
+ e,
555
+ )
529
556
  return False
530
557
 
531
558
 
@@ -542,6 +569,10 @@ async def _reapply_session_model_on_resume(
542
569
  the ACP server's default. This issues ``set_session_model`` so the resumed
543
570
  live session matches the serialized ``acp_model``.
544
571
 
572
+ For unknown/custom providers (e.g. Devin CLI), we fall back to the generic
573
+ ``set_config_option`` method with configId="model", which is a standard ACP
574
+ method that many custom ACP servers support.
575
+
545
576
  The gating mirrors :meth:`ACPAgent.set_acp_model` (attempt for custom/unknown
546
577
  servers and known providers that support runtime switching; skip only known
547
578
  providers that don't), deliberately differing from the initial-selection
@@ -550,7 +581,7 @@ async def _reapply_session_model_on_resume(
550
581
  tolerated (logged) — like the ``load_session`` fallback above — so resume
551
582
  can't break; the session keeps the server default until the next switch.
552
583
 
553
- Returns ``True`` only when ``set_session_model`` was issued and accepted, so
584
+ Returns ``True`` only when a model-setting call was issued and accepted, so
554
585
  the caller knows the resumed live session is actually running ``acp_model``.
555
586
  ``False`` when there is nothing to reapply, the provider doesn't support the
556
587
  switch, or the server rejected the call (swallowed) — in those cases the
@@ -563,7 +594,22 @@ async def _reapply_session_model_on_resume(
563
594
  if provider is not None and not provider.supports_runtime_model_switch:
564
595
  return False
565
596
  try:
566
- await conn.set_session_model(model_id=acp_model, session_id=session_id)
597
+ if provider is not None:
598
+ # Known provider: use set_session_model
599
+ await conn.set_session_model(model_id=acp_model, session_id=session_id)
600
+ else:
601
+ # Unknown/custom provider: try set_config_option as fallback
602
+ await conn.set_config_option(
603
+ config_id="model",
604
+ value=acp_model,
605
+ session_id=session_id,
606
+ )
607
+ logger.info(
608
+ "Reapplied model %r on unknown/custom ACP server %s "
609
+ "via set_config_option",
610
+ acp_model,
611
+ agent_name,
612
+ )
567
613
  return True
568
614
  except ACPRequestError as e:
569
615
  logger.warning(
@@ -1589,10 +1635,10 @@ class ACPAgent(AgentBase):
1589
1635
  """Whether a live, mid-conversation model switch will be attempted.
1590
1636
 
1591
1637
  Tells a client whether to offer the inline picker's live-switch control.
1592
- Kept in lockstep with :meth:`set_acp_model`, which refuses the switch
1593
- only for a *known* provider that declares no support and otherwise
1594
- attempts it optimistically so a custom/unknown ACP server that does
1595
- support ``session/set_model`` isn't needlessly blocked from the picker.
1638
+ ``True`` only for known providers that explicitly declare support for
1639
+ ``session/set_model``. Unknown/custom providers use ``set_config_option``
1640
+ for *initial* model selection but that RPC is a generic config write, not
1641
+ a guaranteed live-switch primitive, so the picker is hidden for them.
1596
1642
  ``False`` before a session exists (nothing to switch yet).
1597
1643
 
1598
1644
  See
@@ -1601,7 +1647,7 @@ class ACPAgent(AgentBase):
1601
1647
  if self._session_id is None:
1602
1648
  return False
1603
1649
  provider = detect_acp_provider_by_agent_name(self._agent_name)
1604
- return provider is None or provider.supports_runtime_model_switch
1650
+ return provider is not None and provider.supports_runtime_model_switch
1605
1651
 
1606
1652
  def get_all_llms(self) -> Generator[LLM]:
1607
1653
  yield self.llm
@@ -584,9 +584,10 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
584
584
  "skipping hook check for legacy conversation state."
585
585
  )
586
586
 
587
- # Prepare LLM messages using the utility function
587
+ # Prepare LLM messages from the cached, incrementally-maintained view.
588
+ # See https://github.com/OpenHands/software-agent-sdk/issues/3053.
588
589
  _messages_or_condensation = prepare_llm_messages(
589
- state.events, condenser=self.condenser, llm=self.llm
590
+ state.view, condenser=self.condenser, llm=self.llm
590
591
  )
591
592
 
592
593
  # Process condensation event before agent sampels another action
@@ -724,8 +725,10 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
724
725
  "skipping hook check for legacy conversation state."
725
726
  )
726
727
 
728
+ # Prepare LLM messages from the cached, incrementally-maintained view.
729
+ # See https://github.com/OpenHands/software-agent-sdk/issues/3053.
727
730
  _messages_or_condensation = await aprepare_llm_messages(
728
- state.events, condenser=self.condenser, llm=self.llm
731
+ state.view, condenser=self.condenser, llm=self.llm
729
732
  )
730
733
 
731
734
  if isinstance(_messages_or_condensation, Condensation):
@@ -955,6 +958,20 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
955
958
  thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] | None = None,
956
959
  responses_reasoning_item: ReasoningItemModel | None = None,
957
960
  ) -> None:
961
+ try:
962
+ json.loads(tool_call.arguments)
963
+ except json.JSONDecodeError:
964
+ tool_call = tool_call.model_copy(
965
+ update={
966
+ "arguments": json.dumps(
967
+ {
968
+ "_openhands_malformed_tool_call": True,
969
+ "error": error,
970
+ }
971
+ )
972
+ }
973
+ )
974
+
958
975
  tc_event = ActionEvent(
959
976
  source="agent",
960
977
  thought=thought or [],
@@ -10,7 +10,7 @@ import shutil
10
10
  import subprocess
11
11
  import textwrap
12
12
  import types
13
- from collections.abc import Collection, Sequence
13
+ from collections.abc import Collection
14
14
  from typing import (
15
15
  TYPE_CHECKING,
16
16
  Annotated,
@@ -24,7 +24,7 @@ from typing import (
24
24
  from openhands.sdk.context.condenser.base import CondenserBase
25
25
  from openhands.sdk.context.view import View
26
26
  from openhands.sdk.conversation.types import ConversationTokenCallbackType
27
- from openhands.sdk.event.base import Event, LLMConvertibleEvent
27
+ from openhands.sdk.event.base import LLMConvertibleEvent
28
28
  from openhands.sdk.event.condenser import Condensation
29
29
  from openhands.sdk.llm import LLM, LLMResponse, Message
30
30
  from openhands.sdk.tool import Action, ToolDefinition
@@ -499,7 +499,7 @@ def normalize_tool_call(
499
499
 
500
500
  @overload
501
501
  def prepare_llm_messages(
502
- events: Sequence[Event],
502
+ view: View,
503
503
  condenser: None = None,
504
504
  additional_messages: list[Message] | None = None,
505
505
  llm: LLM | None = None,
@@ -508,7 +508,7 @@ def prepare_llm_messages(
508
508
 
509
509
  @overload
510
510
  def prepare_llm_messages(
511
- events: Sequence[Event],
511
+ view: View,
512
512
  condenser: CondenserBase,
513
513
  additional_messages: list[Message] | None = None,
514
514
  llm: LLM | None = None,
@@ -516,19 +516,25 @@ def prepare_llm_messages(
516
516
 
517
517
 
518
518
  def prepare_llm_messages(
519
- events: Sequence[Event],
519
+ view: View,
520
520
  condenser: CondenserBase | None = None,
521
521
  additional_messages: list[Message] | None = None,
522
522
  llm: LLM | None = None,
523
523
  ) -> list[Message] | Condensation:
524
- """Prepare LLM messages from conversation context.
524
+ """Prepare LLM messages from a conversation view.
525
525
 
526
526
  This utility function extracts the common logic for preparing conversation
527
527
  context that is shared between agent.step() and ask_agent() methods.
528
528
  It handles condensation internally and calls the callback when needed.
529
529
 
530
+ Callers should pass the cached `ConversationState.view`, which is
531
+ maintained incrementally as events are appended. This avoids paying the
532
+ O(n) `View.from_events` (with `enforce_properties`) cost on every step.
533
+ See https://github.com/OpenHands/software-agent-sdk/issues/3053.
534
+
530
535
  Args:
531
- events: Sequence of events to prepare messages from
536
+ view: A `View` of the conversation history. The view is treated as
537
+ read-only — see `CondenserBase.condense` for the same contract.
532
538
  condenser: Optional condenser for handling context window limits
533
539
  additional_messages: Optional additional messages to append
534
540
  llm: Optional LLM instance from the agent, passed to condenser for
@@ -537,12 +543,7 @@ def prepare_llm_messages(
537
543
  Returns:
538
544
  List of messages ready for LLM completion, or a Condensation event
539
545
  if condensation is needed
540
-
541
- Raises:
542
- RuntimeError: If condensation is needed but no callback is provided
543
546
  """
544
-
545
- view = View.from_events(events)
546
547
  llm_convertible_events: list[LLMConvertibleEvent] = view.events
547
548
 
548
549
  # If a condenser is registered, we need to give it an
@@ -622,7 +623,7 @@ def make_llm_completion(
622
623
 
623
624
 
624
625
  async def aprepare_llm_messages(
625
- events: Sequence[Event],
626
+ view: View,
626
627
  condenser: CondenserBase | None = None,
627
628
  additional_messages: list[Message] | None = None,
628
629
  llm: LLM | None = None,
@@ -632,7 +633,6 @@ async def aprepare_llm_messages(
632
633
  Calls ``condenser.acondense()`` so that condensers backed by an LLM can
633
634
  use async completions without blocking the event loop.
634
635
  """
635
- view = View.from_events(events)
636
636
  llm_convertible_events: list[LLMConvertibleEvent] = view.events
637
637
 
638
638
  if condenser is not None:
@@ -177,9 +177,15 @@ class LLMSummarizingCondenser(RollingCondenser):
177
177
 
178
178
  # Do not pass extra_body explicitly. The LLM handles forwarding
179
179
  # litellm_extra_body only when it is non-empty.
180
- llm_response = self.llm.completion(
181
- messages=messages,
182
- )
180
+ try:
181
+ llm_response = self.llm.completion(
182
+ messages=messages,
183
+ )
184
+ except Exception as e:
185
+ raise NoCondensationAvailableException(
186
+ f"Summarization LLM call failed: {e}"
187
+ ) from e
188
+
183
189
  # Extract summary from the LLMResponse message
184
190
  summary = None
185
191
  if llm_response.message.content:
@@ -364,7 +370,12 @@ class LLMSummarizingCondenser(RollingCondenser):
364
370
  )
365
371
 
366
372
  messages = [Message(role="user", content=[TextContent(text=prompt)])]
367
- llm_response = await self.llm.acompletion(messages=messages)
373
+ try:
374
+ llm_response = await self.llm.acompletion(messages=messages)
375
+ except Exception as e:
376
+ raise NoCondensationAvailableException(
377
+ f"Summarization LLM call failed: {e}"
378
+ ) from e
368
379
 
369
380
  summary = None
370
381
  if llm_response.message.content:
@@ -18,6 +18,7 @@ from openhands.sdk.hooks import HookConfig
18
18
  from openhands.sdk.logger import get_logger
19
19
  from openhands.sdk.plugin import PluginSource
20
20
  from openhands.sdk.secret import SecretValue
21
+ from openhands.sdk.tool.client_tool import ClientToolSpec
21
22
  from openhands.sdk.workspace import LocalWorkspace, RemoteWorkspace
22
23
 
23
24
 
@@ -81,6 +82,7 @@ class Conversation:
81
82
  delete_on_close: bool = True,
82
83
  tags: dict[str, str] | None = None,
83
84
  user_id: str | None = None,
85
+ client_tools: list[ClientToolSpec] | None = None,
84
86
  ) -> "LocalConversation": ...
85
87
 
86
88
  @overload
@@ -106,6 +108,7 @@ class Conversation:
106
108
  delete_on_close: bool = True,
107
109
  tags: dict[str, str] | None = None,
108
110
  user_id: str | None = None,
111
+ client_tools: list[ClientToolSpec] | None = None,
109
112
  ) -> "RemoteConversation": ...
110
113
 
111
114
  def __new__(
@@ -131,6 +134,7 @@ class Conversation:
131
134
  delete_on_close: bool = True,
132
135
  tags: dict[str, str] | None = None,
133
136
  user_id: str | None = None,
137
+ client_tools: list[ClientToolSpec] | None = None,
134
138
  ) -> BaseConversation:
135
139
  from openhands.sdk.conversation.impl.local_conversation import LocalConversation
136
140
  from openhands.sdk.conversation.impl.remote_conversation import (
@@ -185,6 +189,7 @@ class Conversation:
185
189
  delete_on_close=delete_on_close,
186
190
  tags=effective_tags if effective_tags else None,
187
191
  user_id=user_id,
192
+ client_tools=client_tools,
188
193
  )
189
194
 
190
195
  return LocalConversation(
@@ -204,4 +209,5 @@ class Conversation:
204
209
  delete_on_close=delete_on_close,
205
210
  tags=tags,
206
211
  user_id=user_id,
212
+ client_tools=client_tools,
207
213
  )
@@ -2,6 +2,7 @@ import asyncio
2
2
  import atexit
3
3
  import contextlib
4
4
  import copy
5
+ import json
5
6
  import uuid
6
7
  from collections.abc import Mapping
7
8
  from pathlib import Path
@@ -68,6 +69,7 @@ from openhands.sdk.subagent import (
68
69
  register_file_agents,
69
70
  register_plugin_agents,
70
71
  )
72
+ from openhands.sdk.tool.client_tool import ClientToolSpec
71
73
  from openhands.sdk.tool.schema import Action, Observation
72
74
  from openhands.sdk.utils.cipher import Cipher
73
75
  from openhands.sdk.workspace import LocalWorkspace
@@ -148,6 +150,7 @@ class LocalConversation(BaseConversation):
148
150
  cipher: Cipher | None = None,
149
151
  tags: dict[str, str] | None = None,
150
152
  user_id: str | None = None,
153
+ client_tools: list[ClientToolSpec] | None = None,
151
154
  **_: object,
152
155
  ):
153
156
  """Initialize the conversation.
@@ -189,6 +192,11 @@ class LocalConversation(BaseConversation):
189
192
  (lost) on serialization.
190
193
  tags: Optional key-value tags for the conversation. Keys must be
191
194
  lowercase alphanumeric, values up to 256 characters.
195
+ client_tools: Optional list of client-defined tool specs. Each spec
196
+ is registered and injected into the agent so it can call the
197
+ tool; the executor returns an acknowledgment and the real
198
+ execution is expected to be handled by a callback/consumer
199
+ (e.g. a frontend) observing the emitted ActionEvent.
192
200
  """
193
201
  super().__init__() # Initialize with span tracking
194
202
  # Mark cleanup as initiated as early as possible to avoid races or partially
@@ -206,6 +214,32 @@ class LocalConversation(BaseConversation):
206
214
  self._pending_hook_config = hook_config # Will be combined with plugin hooks
207
215
  self._agent_ready = False # Agent initialized lazily after plugins loaded
208
216
 
217
+ # Create-or-resume: factory inspects BASE_STATE to decide
218
+ desired_id = conversation_id or uuid.uuid4()
219
+
220
+ # Resolve client-defined tools, then register them and inject the matching
221
+ # Tool specs into the agent so the agent can call them. Execution is
222
+ # deferred to a consumer of the emitted ActionEvent (e.g. a frontend); the
223
+ # executor only acks. Specs come either from the caller (`client_tools`)
224
+ # or, when resuming a persisted conversation without re-supplying them,
225
+ # from the persisted agent's tool specs — mirroring the server resume
226
+ # path so a fresh process can re-register the dynamic tools.
227
+ resolved_client_tools = list(client_tools or [])
228
+ if not resolved_client_tools and persistence_dir is not None:
229
+ resolved_client_tools = self._recover_persisted_client_tools(
230
+ persistence_dir, desired_id
231
+ )
232
+ if resolved_client_tools:
233
+ from openhands.sdk.tool.client_tool import register_client_tools
234
+
235
+ client_tool_specs = register_client_tools(resolved_client_tools)
236
+ existing_names = {t.name for t in agent.tools}
237
+ new_tools = [
238
+ ts for ts in client_tool_specs if ts.name not in existing_names
239
+ ]
240
+ if new_tools:
241
+ agent = agent.model_copy(update={"tools": [*agent.tools, *new_tools]})
242
+
209
243
  self.agent = agent
210
244
  if isinstance(workspace, (str, Path)):
211
245
  # LocalWorkspace accepts both str and Path via BeforeValidator
@@ -217,9 +251,6 @@ class LocalConversation(BaseConversation):
217
251
  ws_path = Path(self.workspace.working_dir)
218
252
  if not ws_path.exists():
219
253
  ws_path.mkdir(parents=True, exist_ok=True)
220
-
221
- # Create-or-resume: factory inspects BASE_STATE to decide
222
- desired_id = conversation_id or uuid.uuid4()
223
254
  self._state = ConversationState.create(
224
255
  id=desired_id,
225
256
  agent=agent,
@@ -344,6 +375,44 @@ class LocalConversation(BaseConversation):
344
375
  self._start_observability_span(str(desired_id), user_id=user_id)
345
376
  self.delete_on_close = delete_on_close
346
377
 
378
+ def _recover_persisted_client_tools(
379
+ self,
380
+ persistence_base_dir: str | Path,
381
+ conversation_id: ConversationID,
382
+ ) -> list[ClientToolSpec]:
383
+ """Recover client tool specs from a persisted conversation's base state.
384
+
385
+ When a persisted conversation is resumed in a fresh process, the dynamic
386
+ client tools are absent from the global registry and the caller may not
387
+ re-supply ``client_tools``. Without recovery, the persisted agent's
388
+ client tools would appear "removed" and resume would fail. We read the
389
+ persisted agent tool specs and pull out the embedded ``ClientToolSpec``s
390
+ so they can be re-registered and re-injected. Returns an empty list when
391
+ there is no persisted state yet (fresh conversation).
392
+ """
393
+ from pydantic import ValidationError
394
+
395
+ from openhands.sdk.conversation.persistence_const import BASE_STATE
396
+ from openhands.sdk.tool.client_tool import extract_client_tool_specs
397
+ from openhands.sdk.tool.spec import Tool
398
+
399
+ base_path = (
400
+ Path(self.get_persistence_dir(persistence_base_dir, conversation_id))
401
+ / BASE_STATE
402
+ )
403
+ try:
404
+ data = json.loads(base_path.read_text())
405
+ except (FileNotFoundError, json.JSONDecodeError):
406
+ return []
407
+ raw_tools = (data.get("agent") or {}).get("tools") or []
408
+ tools: list[Tool] = []
409
+ for raw_tool in raw_tools:
410
+ try:
411
+ tools.append(Tool.model_validate(raw_tool))
412
+ except ValidationError:
413
+ continue
414
+ return extract_client_tool_specs(tools)
415
+
347
416
  @property
348
417
  def id(self) -> ConversationID:
349
418
  """Get the unique ID of the conversation."""
@@ -1798,7 +1867,7 @@ class LocalConversation(BaseConversation):
1798
1867
  )
1799
1868
 
1800
1869
  messages = prepare_llm_messages(
1801
- self.state.events, additional_messages=[user_message]
1870
+ self.state.view, additional_messages=[user_message]
1802
1871
  )
1803
1872
 
1804
1873
  # Get or create the specialized ask-agent LLM
@@ -53,6 +53,7 @@ from openhands.sdk.security.analyzer import SecurityAnalyzerBase
53
53
  from openhands.sdk.security.confirmation_policy import (
54
54
  ConfirmationPolicyBase,
55
55
  )
56
+ from openhands.sdk.tool.client_tool import ClientTool, ClientToolSpec
56
57
  from openhands.sdk.utils.redact import http_error_log_content
57
58
  from openhands.sdk.workspace import LocalWorkspace, RemoteWorkspace
58
59
 
@@ -671,6 +672,7 @@ class RemoteConversation(BaseConversation):
671
672
  delete_on_close: bool = False,
672
673
  tags: dict[str, str] | None = None,
673
674
  user_id: str | None = None,
675
+ client_tools: list[ClientToolSpec] | None = None,
674
676
  **_: object,
675
677
  ) -> None:
676
678
  """Remote conversation proxy that talks to an agent server.
@@ -700,6 +702,10 @@ class RemoteConversation(BaseConversation):
700
702
  tags: Optional key-value tags for the conversation. Keys must be
701
703
  lowercase alphanumeric, values up to 256 characters.
702
704
  user_id: Optional user ID to associate with observability traces
705
+ client_tools: Optional list of client-defined tool specs. These tools
706
+ have no server-side executor — when the agent calls them an
707
+ ActionEvent is emitted over the WebSocket and the client
708
+ handles execution via callbacks.
703
709
  """
704
710
  super().__init__() # Initialize base class with span tracking
705
711
  self.agent = agent
@@ -713,6 +719,12 @@ class RemoteConversation(BaseConversation):
713
719
  self._terminal_status_queue: Queue[str] = Queue()
714
720
  self._run_armed = threading.Event()
715
721
 
722
+ # Client tool specs the server already has persisted for this
723
+ # conversation (populated when re-attaching to an existing one). These
724
+ # must be registered locally before the initial event sync so that
725
+ # persisted ``ClientAction_*`` events can be deserialized.
726
+ attached_client_tools: list[ClientToolSpec] = []
727
+
716
728
  should_create = conversation_id is None
717
729
  if conversation_id is not None:
718
730
  # Try to attach to existing conversation
@@ -726,11 +738,18 @@ class RemoteConversation(BaseConversation):
726
738
  # Conversation doesn't exist, we'll create it
727
739
  should_create = True
728
740
  else:
729
- agent_payload = resp.json().get("agent")
741
+ info = resp.json()
742
+ agent_payload = info.get("agent")
730
743
  if agent_payload is not None:
731
744
  remote_agent = _validate_remote_agent(agent_payload)
732
745
  if remote_agent.agent_kind != agent.agent_kind:
733
746
  raise ValueError(_agent_kind_mismatch_message(conversation_id))
747
+ # Capture persisted client tool specs so we can register their
748
+ # dynamic action types before RemoteState syncs events.
749
+ for raw_spec in info.get("client_tools") or []:
750
+ attached_client_tools.append(
751
+ ClientToolSpec.model_validate(raw_spec)
752
+ )
734
753
  # Conversation exists, use the provided ID
735
754
  self._id = conversation_id
736
755
 
@@ -765,6 +784,12 @@ class RemoteConversation(BaseConversation):
765
784
  "plugins": [p.model_dump() for p in plugins] if plugins else None,
766
785
  # Include hook_config for server-side hooks
767
786
  "hook_config": hook_config.model_dump() if hook_config else None,
787
+ # Include client-defined tool specs (no server-side executor)
788
+ "client_tools": (
789
+ [s.model_dump(mode="json") for s in client_tools]
790
+ if client_tools
791
+ else []
792
+ ),
768
793
  # Include tags if provided
769
794
  "tags": tags or {},
770
795
  }
@@ -797,6 +822,18 @@ class RemoteConversation(BaseConversation):
797
822
 
798
823
  workspace.register_conversation(str(self._id))
799
824
 
825
+ # Register client tool action types locally so WebSocket/persisted
826
+ # events with ClientAction_* action_type can be deserialized by the
827
+ # event loop. This must cover both the specs the caller passed in and
828
+ # the specs the server already had persisted (when re-attaching), so a
829
+ # plain reattach by conversation_id can still sync persisted events.
830
+ seen_client_tool_names: set[str] = set()
831
+ for spec in [*(client_tools or []), *attached_client_tools]:
832
+ if spec.name in seen_client_tool_names:
833
+ continue
834
+ seen_client_tool_names.add(spec.name)
835
+ ClientTool.from_spec(spec)
836
+
800
837
  # Initialize the remote state
801
838
  self._state = RemoteState(
802
839
  self._client,
@@ -27,6 +27,7 @@ from openhands.sdk.security.confirmation_policy import (
27
27
  NeverConfirm,
28
28
  )
29
29
  from openhands.sdk.subagent.schema import AgentDefinition
30
+ from openhands.sdk.tool.client_tool import ClientToolSpec
30
31
  from openhands.sdk.utils.models import kind_of
31
32
  from openhands.sdk.workspace import LocalWorkspace
32
33
 
@@ -138,6 +139,16 @@ class StartConversationRequest(BaseModel):
138
139
  "to register the tools for this conversation."
139
140
  ),
140
141
  )
142
+ client_tools: list[ClientToolSpec] = Field(
143
+ default_factory=list,
144
+ description=(
145
+ "Tools defined by the client via JSON spec. These tools have "
146
+ "no server-side executor — when the agent calls them, an "
147
+ "ActionEvent is emitted over the WebSocket and the client "
148
+ "handles execution. The SDK returns an acknowledgment "
149
+ "observation immediately."
150
+ ),
151
+ )
141
152
  agent_definitions: list[AgentDefinition] = Field(
142
153
  default_factory=list,
143
154
  description=(
@@ -5,6 +5,7 @@ from litellm.exceptions import (
5
5
  AuthenticationError,
6
6
  BadRequestError,
7
7
  ContextWindowExceededError,
8
+ InternalServerError,
8
9
  OpenAIError,
9
10
  PermissionDeniedError,
10
11
  )
@@ -48,6 +49,9 @@ MALFORMED_HISTORY_PATTERNS: list[str] = [
48
49
  ),
49
50
  # Moonshot / Kimi variant
50
51
  "must be followed by tool messages responding to each 'tool_call_id'",
52
+ # OpenAI-compatible providers may reject replayed assistant tool calls whose
53
+ # arguments are not valid JSON.
54
+ "failed to parse tool call arguments as json",
51
55
  ]
52
56
 
53
57
 
@@ -58,7 +62,10 @@ def is_context_window_exceeded(exception: Exception) -> bool:
58
62
  # Check for litellm/openai exception types that may contain context window errors.
59
63
  # APIConnectionError can wrap provider-specific errors (e.g., Minimax) that include
60
64
  # context window messages in their error text.
61
- if not isinstance(exception, (BadRequestError, OpenAIError, APIConnectionError)):
65
+ if not isinstance(
66
+ exception,
67
+ (BadRequestError, OpenAIError, APIConnectionError, InternalServerError),
68
+ ):
62
69
  return False
63
70
 
64
71
  s = str(exception).lower()
@@ -69,7 +76,10 @@ def looks_like_malformed_conversation_history_error(exception: Exception) -> boo
69
76
  if isinstance(exception, LLMMalformedConversationHistoryError):
70
77
  return True
71
78
 
72
- if not isinstance(exception, (BadRequestError, OpenAIError, APIConnectionError)):
79
+ if not isinstance(
80
+ exception,
81
+ (BadRequestError, OpenAIError, APIConnectionError, InternalServerError),
82
+ ):
73
83
  return False
74
84
 
75
85
  s = str(exception).lower()