openhands-sdk 1.29.3__tar.gz → 1.30.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 (280) hide show
  1. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/__init__.py +2 -0
  3. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/agent.py +74 -2
  4. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/base.py +36 -13
  5. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/utils.py +8 -0
  6. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/agent_context.py +10 -5
  7. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +10 -0
  8. openhands_sdk-1.30.0/openhands/sdk/context/prompts/presets.py +109 -0
  9. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/sections/__init__.py +4 -1
  10. openhands_sdk-1.29.3/openhands/sdk/agent/prompts/system_prompt_planning.j2 → openhands_sdk-1.30.0/openhands/sdk/context/prompts/sections/planning.py +40 -2
  11. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/base.py +20 -1
  12. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/conversation.py +12 -1
  13. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/impl/local_conversation.py +48 -27
  14. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/impl/remote_conversation.py +13 -0
  15. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/persistence_const.py +2 -1
  16. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/request.py +9 -0
  17. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/title_utils.py +5 -1
  18. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/types.py +29 -0
  19. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/__init__.py +7 -1
  20. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/auth/openai.py +17 -2
  21. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/llm.py +91 -9
  22. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/llm_profile_store.py +28 -1
  23. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/options/chat_options.py +21 -4
  24. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/options/responses_options.py +18 -3
  25. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/router/base.py +9 -0
  26. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/marketplace/registration.py +11 -2
  27. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/tool.py +30 -4
  28. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/observability/laminar.py +23 -0
  29. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/types.py +10 -1
  30. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/profiles/__init__.py +14 -0
  31. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/profiles/agent_profile.py +35 -0
  32. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/profiles/agent_profile_store.py +131 -2
  33. openhands_sdk-1.30.0/openhands/sdk/profiles/profile_refs.py +149 -0
  34. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/profiles/resolver.py +3 -3
  35. openhands_sdk-1.30.0/openhands/sdk/profiles/seed.py +71 -0
  36. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/settings/model.py +8 -0
  37. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/subagent/__init__.py +4 -1
  38. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/subagent/load.py +45 -0
  39. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/subagent/schema.py +10 -1
  40. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/testing/test_llm.py +4 -0
  41. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/redact.py +14 -10
  42. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  43. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands_sdk.egg-info/SOURCES.txt +4 -2
  44. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/pyproject.toml +1 -1
  45. openhands_sdk-1.29.3/openhands/sdk/context/prompts/presets.py +0 -71
  46. openhands_sdk-1.29.3/openhands/sdk/profiles/profile_refs.py +0 -173
  47. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/__init__.py +0 -0
  48. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/acp_agent.py +0 -0
  49. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/acp_models.py +0 -0
  50. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  51. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  52. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  53. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/banner.py +0 -0
  54. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/__init__.py +0 -0
  55. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  56. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/base.py +0 -0
  57. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  58. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  59. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  60. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/condenser/utils.py +0 -0
  61. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  62. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  63. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/registry.py +0 -0
  64. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/section.py +0 -0
  65. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  66. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
  67. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  68. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  69. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/__init__.py +0 -0
  70. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  71. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  72. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/base.py +0 -0
  73. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  74. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  75. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  76. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  77. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/context/view/view.py +0 -0
  78. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/__init__.py +0 -0
  79. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/cancellation.py +0 -0
  80. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  81. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/event_store.py +0 -0
  82. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  83. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/exceptions.py +0 -0
  84. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  85. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  86. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/goal/controller.py +0 -0
  87. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  88. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  89. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  90. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  91. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  92. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/response_utils.py +0 -0
  93. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  94. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/state.py +0 -0
  95. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  96. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  97. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  98. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  99. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/__init__.py +0 -0
  100. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/base.py +0 -0
  101. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  102. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  103. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  104. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  105. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  106. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  107. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  108. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  109. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  110. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/critic/result.py +0 -0
  111. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/__init__.py +0 -0
  112. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  113. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/base.py +0 -0
  114. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/condenser.py +0 -0
  115. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/conversation_error.py +0 -0
  116. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/conversation_state.py +0 -0
  117. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/hook_execution.py +0 -0
  118. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  119. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  120. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  121. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  122. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  123. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  124. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  125. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/resume_transcript.py +0 -0
  126. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/streaming_delta.py +0 -0
  127. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/token.py +0 -0
  128. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/types.py +0 -0
  129. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/event/user_action.py +0 -0
  130. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/__init__.py +0 -0
  131. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/fetch.py +0 -0
  132. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  133. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/info.py +0 -0
  134. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  135. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  136. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  137. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  138. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/cached_repo.py +0 -0
  139. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/exceptions.py +0 -0
  140. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/git_changes.py +0 -0
  141. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/git_diff.py +0 -0
  142. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/models.py +0 -0
  143. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/git/utils.py +0 -0
  144. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/__init__.py +0 -0
  145. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/config.py +0 -0
  146. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  147. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/executor.py +0 -0
  148. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/manager.py +0 -0
  149. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/hooks/types.py +0 -0
  150. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/io/__init__.py +0 -0
  151. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/io/base.py +0 -0
  152. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/io/cache.py +0 -0
  153. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/io/local.py +0 -0
  154. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/io/memory.py +0 -0
  155. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  156. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  157. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  158. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  159. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  160. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  161. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  162. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/llm_registry.py +0 -0
  163. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/llm_response.py +0 -0
  164. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/message.py +0 -0
  165. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  166. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  167. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  168. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/options/__init__.py +0 -0
  169. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/options/common.py +0 -0
  170. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/router/__init__.py +0 -0
  171. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  172. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  173. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/streaming.py +0 -0
  174. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  175. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  176. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  177. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  178. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/model_features.py +0 -0
  179. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  180. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  181. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
  182. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  183. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  184. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  185. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  186. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
  187. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  188. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/logger/__init__.py +0 -0
  189. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/logger/logger.py +0 -0
  190. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/logger/rolling.py +0 -0
  191. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/marketplace/__init__.py +0 -0
  192. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/marketplace/registry.py +0 -0
  193. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/marketplace/types.py +0 -0
  194. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/__init__.py +0 -0
  195. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/client.py +0 -0
  196. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/definition.py +0 -0
  197. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/exceptions.py +0 -0
  198. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/mcp/utils.py +0 -0
  199. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/observability/__init__.py +0 -0
  200. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/observability/utils.py +0 -0
  201. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/__init__.py +0 -0
  202. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/discovery.py +0 -0
  203. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/fetch.py +0 -0
  204. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/installed.py +0 -0
  205. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/loader.py +0 -0
  206. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/plugin.py +0 -0
  207. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/plugin/source.py +0 -0
  208. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/py.typed +0 -0
  209. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/secret/__init__.py +0 -0
  210. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/secret/secrets.py +0 -0
  211. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/__init__.py +0 -0
  212. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/_shell_ast.py +0 -0
  213. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/analyzer.py +0 -0
  214. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  215. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  216. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  217. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  218. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  219. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/ensemble.py +0 -0
  220. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  221. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  222. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  223. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  224. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/risk.py +0 -0
  225. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/security/shell_parser.py +0 -0
  226. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/settings/__init__.py +0 -0
  227. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/settings/acp_providers.py +0 -0
  228. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/settings/api_models.py +0 -0
  229. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/settings/metadata.py +0 -0
  230. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/__init__.py +0 -0
  231. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/exceptions.py +0 -0
  232. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/execute.py +0 -0
  233. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/fetch.py +0 -0
  234. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/installed.py +0 -0
  235. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/skill.py +0 -0
  236. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/trigger.py +0 -0
  237. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/types.py +0 -0
  238. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/skills/utils.py +0 -0
  239. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/subagent/registry.py +0 -0
  240. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/testing/__init__.py +0 -0
  241. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/__init__.py +0 -0
  242. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  243. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  244. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  245. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  246. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/builtins/think.py +0 -0
  247. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/client_tool.py +0 -0
  248. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/registry.py +0 -0
  249. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/schema.py +0 -0
  250. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/spec.py +0 -0
  251. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/tool/tool.py +0 -0
  252. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/__init__.py +0 -0
  253. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/async_executor.py +0 -0
  254. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/async_utils.py +0 -0
  255. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/cipher.py +0 -0
  256. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/command.py +0 -0
  257. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/datetime.py +0 -0
  258. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/deprecation.py +0 -0
  259. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/github.py +0 -0
  260. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/json.py +0 -0
  261. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/models.py +0 -0
  262. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/paging.py +0 -0
  263. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/path.py +0 -0
  264. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  265. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/truncate.py +0 -0
  266. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/utils/visualize.py +0 -0
  267. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/__init__.py +0 -0
  268. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/base.py +0 -0
  269. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/local.py +0 -0
  270. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/models.py +0 -0
  271. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  272. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  273. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/remote/base.py +0 -0
  274. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  275. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/repo.py +0 -0
  276. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands/sdk/workspace/workspace.py +0 -0
  277. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  278. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands_sdk.egg-info/requires.txt +0 -0
  279. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  280. {openhands_sdk-1.29.3 → openhands_sdk-1.30.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.29.3
3
+ Version: 1.30.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
@@ -85,6 +85,7 @@ from openhands.sdk.skills import (
85
85
  )
86
86
  from openhands.sdk.subagent import (
87
87
  agent_definition_to_factory,
88
+ discover_agents,
88
89
  load_agents_from_dir,
89
90
  load_project_agents,
90
91
  load_user_agents,
@@ -197,6 +198,7 @@ __all__ = [
197
198
  "load_project_agents",
198
199
  "load_user_agents",
199
200
  "load_agents_from_dir",
201
+ "discover_agents",
200
202
  "agent_definition_to_factory",
201
203
  "load_project_skills",
202
204
  "load_skills_from_dir",
@@ -27,7 +27,7 @@ from openhands.sdk.agent.utils import (
27
27
  parse_tool_call_arguments,
28
28
  prepare_llm_messages,
29
29
  )
30
- from openhands.sdk.context.prompts.presets import create_registry
30
+ from openhands.sdk.context.prompts.presets import PromptPreset, create_registry
31
31
  from openhands.sdk.conversation import (
32
32
  CancellationToken,
33
33
  ConversationCallbackType,
@@ -51,6 +51,7 @@ from openhands.sdk.event.condenser import (
51
51
  CondensationRequest,
52
52
  )
53
53
  from openhands.sdk.llm import (
54
+ LLM,
54
55
  LLMResponse,
55
56
  Message,
56
57
  MessageToolCall,
@@ -65,6 +66,7 @@ from openhands.sdk.llm.exceptions import (
65
66
  LLMContextWindowExceedError,
66
67
  LLMMalformedConversationHistoryError,
67
68
  )
69
+ from openhands.sdk.llm.router.base import RouterLLM
68
70
  from openhands.sdk.logger import get_logger
69
71
  from openhands.sdk.observability.laminar import (
70
72
  maybe_init_laminar,
@@ -79,6 +81,7 @@ from openhands.sdk.tool import (
79
81
 
80
82
 
81
83
  if TYPE_CHECKING:
84
+ from openhands.sdk.llm.llm import LLMCallContext
82
85
  from openhands.sdk.tool import ToolDefinition
83
86
  from openhands.sdk.mcp.tool import MCPToolDefinition
84
87
  from openhands.sdk.tool.builtins import (
@@ -113,6 +116,36 @@ def _tool_has_summary_param(tool: ToolDefinition) -> bool:
113
116
  INIT_STATE_PREFIX_SCAN_WINDOW = 3
114
117
 
115
118
 
119
+ def _latest_user_message_contains_image(messages: list[Message]) -> bool:
120
+ for message in reversed(messages):
121
+ if message.role == "user":
122
+ return message.contains_image
123
+ return False
124
+
125
+
126
+ def _non_multimodal_image_message(model: str) -> Message:
127
+ return Message(
128
+ role="assistant",
129
+ content=[
130
+ TextContent(
131
+ text=(
132
+ "I received your image, but the currently selected model "
133
+ f"({model}) does not support image understanding. Please "
134
+ "switch to a multimodal model to analyze the image."
135
+ )
136
+ )
137
+ ],
138
+ )
139
+
140
+
141
+ def _should_handle_non_multimodal_image_input(
142
+ llm: LLM, messages: list[Message]
143
+ ) -> bool:
144
+ if isinstance(llm, RouterLLM):
145
+ return False
146
+ return _latest_user_message_contains_image(messages) and not llm.vision_is_active()
147
+
148
+
116
149
  @dataclass(frozen=True, slots=True)
117
150
  class _ActionBatch:
118
151
  """Immutable result of preparing a batch of actions for execution.
@@ -474,7 +507,10 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
474
507
  secret_infos = state.secret_registry.get_secret_infos()
475
508
 
476
509
  ctx = self._build_prompt_context(additional_secret_infos=secret_infos)
477
- return create_registry().build(ctx).dynamic
510
+ # The dynamic tier is preset-independent; fall back to the default tier for a
511
+ # custom Jinja template (preset None), as before.
512
+ preset = self._prompt_preset or PromptPreset.DEFAULT
513
+ return create_registry(preset).build(ctx).dynamic
478
514
 
479
515
  def _execute_actions(
480
516
  self,
@@ -572,6 +608,10 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
572
608
  "skipping hook check for legacy conversation state."
573
609
  )
574
610
 
611
+ # Build per-conversation context once and thread it through all
612
+ # LLM calls in this step (avoids shared mutable state on the LLM).
613
+ call_context: LLMCallContext = conversation.get_llm_call_context()
614
+
575
615
  # Prepare LLM messages from the cached, incrementally-maintained view.
576
616
  # See https://github.com/OpenHands/software-agent-sdk/issues/3053.
577
617
  _messages_or_condensation = prepare_llm_messages(
@@ -585,6 +625,20 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
585
625
 
586
626
  _messages = _messages_or_condensation
587
627
 
628
+ if _should_handle_non_multimodal_image_input(self.llm, _messages):
629
+ logger.info(
630
+ "Image input received while selected model does not support vision: %s",
631
+ self.llm.model,
632
+ )
633
+ on_event(
634
+ MessageEvent(
635
+ source="agent",
636
+ llm_message=_non_multimodal_image_message(self.llm.model),
637
+ )
638
+ )
639
+ state.execution_status = ConversationExecutionStatus.FINISHED
640
+ return
641
+
588
642
  logger.debug(
589
643
  "Sending messages to LLM: "
590
644
  f"{json.dumps([m.model_dump() for m in _messages[1:]], indent=2)}"
@@ -596,6 +650,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
596
650
  _messages,
597
651
  tools=list(self.tools_map.values()),
598
652
  on_token=on_token,
653
+ call_context=call_context,
599
654
  )
600
655
  except FunctionCallValidationError as e:
601
656
  logger.warning(f"LLM generated malformed function call: {e}")
@@ -735,6 +790,8 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
735
790
  "skipping hook check for legacy conversation state."
736
791
  )
737
792
 
793
+ call_context: LLMCallContext = conversation.get_llm_call_context()
794
+
738
795
  # Prepare LLM messages from the cached, incrementally-maintained view.
739
796
  # See https://github.com/OpenHands/software-agent-sdk/issues/3053.
740
797
  _messages_or_condensation = await aprepare_llm_messages(
@@ -747,6 +804,20 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
747
804
 
748
805
  _messages = _messages_or_condensation
749
806
 
807
+ if _should_handle_non_multimodal_image_input(self.llm, _messages):
808
+ logger.info(
809
+ "Image input received while selected model does not support vision: %s",
810
+ self.llm.model,
811
+ )
812
+ on_event(
813
+ MessageEvent(
814
+ source="agent",
815
+ llm_message=_non_multimodal_image_message(self.llm.model),
816
+ )
817
+ )
818
+ state.execution_status = ConversationExecutionStatus.FINISHED
819
+ return
820
+
750
821
  logger.debug(
751
822
  "Sending messages to LLM: "
752
823
  f"{json.dumps([m.model_dump() for m in _messages[1:]], indent=2)}"
@@ -758,6 +829,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
758
829
  _messages,
759
830
  tools=list(self.tools_map.values()),
760
831
  on_token=on_token,
832
+ call_context=call_context,
761
833
  )
762
834
  except FunctionCallValidationError as e:
763
835
  logger.warning(f"LLM generated malformed function call: {e}")
@@ -26,7 +26,7 @@ from pydantic import (
26
26
 
27
27
  from openhands.sdk.context.agent_context import AgentContext
28
28
  from openhands.sdk.context.condenser import CondenserBase
29
- from openhands.sdk.context.prompts.presets import create_registry
29
+ from openhands.sdk.context.prompts.presets import PromptPreset, create_registry
30
30
  from openhands.sdk.context.prompts.prompt import render_template
31
31
  from openhands.sdk.context.prompts.section import Platform, PromptContext
32
32
  from openhands.sdk.critic.base import CriticBase
@@ -111,12 +111,21 @@ _DEFAULT_SOUL = (
111
111
  " with a computer to solve tasks."
112
112
  )
113
113
 
114
- # Built-in prompt dir. The registry only stands in for the default prompt here; a
115
- # subclass with its own prompts/system_prompt.j2 keeps the Jinja render path.
114
+ # Built-in prompt dir. The registry only stands in for built-in prompts here; a
115
+ # subclass with its own prompts/ keeps the Jinja render path.
116
116
  _BUILTIN_PROMPT_DIR = os.path.realpath(
117
117
  os.path.join(os.path.dirname(__file__), "prompts")
118
118
  )
119
119
 
120
+ # Built-in ``system_prompt_filename`` values are back-compat sentinels (the .j2 files
121
+ # were removed) that select a registry preset. ``system_prompt_planning.j2`` keeps its
122
+ # historical name so ``get_planning_agent`` needs no change. Any other filename -- or a
123
+ # subclass's own ``prompt_dir`` -- falls through to the Jinja escape hatch.
124
+ _PRESET_BY_FILENAME: dict[str, PromptPreset] = {
125
+ "system_prompt.j2": PromptPreset.DEFAULT,
126
+ "system_prompt_planning.j2": PromptPreset.PLANNING,
127
+ }
128
+
120
129
 
121
130
  def _load_soul_md() -> str:
122
131
  """Load ``~/.openhands/SOUL.md``, falling back to the built-in default."""
@@ -461,6 +470,18 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
461
470
  """Returns the name of the Agent."""
462
471
  return self.__class__.__name__
463
472
 
473
+ @property
474
+ def _prompt_preset(self) -> PromptPreset | None:
475
+ """The registry preset for this agent's built-in prompt.
476
+
477
+ ``None`` means "take the Jinja escape hatch": a subclass with its own
478
+ ``prompt_dir``, or a ``system_prompt_filename`` that is not a known built-in
479
+ sentinel (e.g. a custom relative name or an absolute path).
480
+ """
481
+ if os.path.realpath(self.prompt_dir) != _BUILTIN_PROMPT_DIR:
482
+ return None
483
+ return _PRESET_BY_FILENAME.get(self.system_prompt_filename)
484
+
464
485
  @property
465
486
  def static_system_message(self) -> str:
466
487
  """Compute the static portion of the system message.
@@ -469,10 +490,11 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
469
490
  per-conversation context. This static portion can be cached and reused
470
491
  across conversations for better prompt caching efficiency.
471
492
 
472
- The default prompt is assembled from the typed section registry, which also
473
- resolves a custom ``security_policy_filename``. Escape hatches keep the Jinja
474
- path: an inline ``system_prompt`` is returned verbatim; a custom
475
- ``system_prompt_filename`` or subclass ``prompt_dir`` renders its own template.
493
+ Built-in prompts (the ``default`` and ``planning`` presets) are assembled from
494
+ the typed section registry, which also resolves a custom
495
+ ``security_policy_filename``. Escape hatches keep the Jinja path: an inline
496
+ ``system_prompt`` is returned verbatim; a custom ``system_prompt_filename`` or
497
+ subclass ``prompt_dir`` renders its own template.
476
498
 
477
499
  Returns:
478
500
  The static system prompt without dynamic context.
@@ -482,17 +504,15 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
482
504
 
483
505
  # Escape hatch: a custom filename or a subclass's own prompt_dir renders its
484
506
  # own Jinja template; everything else (incl. custom policies) uses the registry.
485
- if (
486
- self.system_prompt_filename != "system_prompt.j2"
487
- or os.path.realpath(self.prompt_dir) != _BUILTIN_PROMPT_DIR
488
- ):
507
+ preset = self._prompt_preset
508
+ if preset is None:
489
509
  return render_template(
490
510
  prompt_dir=self.prompt_dir,
491
511
  template_name=self.system_prompt_filename,
492
512
  **self._resolved_template_kwargs(),
493
513
  )
494
514
 
495
- return create_registry().build(self._build_prompt_context()).static
515
+ return create_registry(preset).build(self._build_prompt_context()).static
496
516
 
497
517
  def _resolved_template_kwargs(self) -> dict[str, object]:
498
518
  """Resolve the system-prompt template kwargs.
@@ -640,7 +660,10 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
640
660
  """
641
661
  if not self.agent_context:
642
662
  return None
643
- return create_registry().build(self._build_prompt_context()).dynamic
663
+ # The dynamic tier is preset-independent, so a custom Jinja template (preset
664
+ # None) still gets the default dynamic block, exactly as before.
665
+ preset = self._prompt_preset or PromptPreset.DEFAULT
666
+ return create_registry(preset).build(self._build_prompt_context()).dynamic
644
667
 
645
668
  def init_state(
646
669
  self,
@@ -31,6 +31,7 @@ from openhands.sdk.tool import Action, ToolDefinition
31
31
 
32
32
 
33
33
  if TYPE_CHECKING:
34
+ from openhands.sdk.llm.llm import LLMCallContext
34
35
  from openhands.sdk.llm.streaming import AnyTokenCallbackType
35
36
 
36
37
 
@@ -604,6 +605,7 @@ def make_llm_completion(
604
605
  messages: list[Message],
605
606
  tools: list[ToolDefinition] | None = None,
606
607
  on_token: ConversationTokenCallbackType | None = None,
608
+ call_context: LLMCallContext | None = None,
607
609
  ) -> LLMResponse:
608
610
  """Make an LLM completion call with the provided messages and tools.
609
611
 
@@ -612,6 +614,7 @@ def make_llm_completion(
612
614
  messages: The messages to send to the LLM
613
615
  tools: Optional list of tools to provide to the LLM
614
616
  on_token: Optional callback for streaming token updates
617
+ call_context: Per-conversation context for cache/session affinity.
615
618
 
616
619
  Returns:
617
620
  LLMResponse from the LLM completion call
@@ -636,6 +639,7 @@ def make_llm_completion(
636
639
  store=False,
637
640
  add_security_risk_prediction=True,
638
641
  on_token=on_token,
642
+ call_context=call_context,
639
643
  )
640
644
  else:
641
645
  return llm.completion(
@@ -643,6 +647,7 @@ def make_llm_completion(
643
647
  tools=tools or [],
644
648
  add_security_risk_prediction=True,
645
649
  on_token=on_token,
650
+ call_context=call_context,
646
651
  )
647
652
 
648
653
 
@@ -686,6 +691,7 @@ async def amake_llm_completion(
686
691
  messages: list[Message],
687
692
  tools: list[ToolDefinition] | None = None,
688
693
  on_token: AnyTokenCallbackType | None = None,
694
+ call_context: LLMCallContext | None = None,
689
695
  ) -> LLMResponse:
690
696
  """Async variant of :func:`make_llm_completion`."""
691
697
  if llm.uses_responses_api():
@@ -696,6 +702,7 @@ async def amake_llm_completion(
696
702
  store=False,
697
703
  add_security_risk_prediction=True,
698
704
  on_token=on_token,
705
+ call_context=call_context,
699
706
  )
700
707
  else:
701
708
  return await llm.acompletion(
@@ -703,4 +710,5 @@ async def amake_llm_completion(
703
710
  tools=tools or [],
704
711
  add_security_risk_prediction=True,
705
712
  on_token=on_token,
713
+ call_context=call_context,
706
714
  )
@@ -118,7 +118,8 @@ class AgentContext(BaseModel):
118
118
  default_factory=list,
119
119
  description=(
120
120
  "Marketplace registrations for plugin resolution. Registrations with "
121
- "auto_load=True are resolved by LocalConversation at startup."
121
+ "auto_load=True or a list of plugin names are resolved by "
122
+ "LocalConversation at startup."
122
123
  ),
123
124
  json_schema_extra={"acp_compatible": True},
124
125
  )
@@ -149,8 +150,8 @@ class AgentContext(BaseModel):
149
150
  json_schema_extra={"acp_compatible": True},
150
151
  )
151
152
  current_datetime: datetime | str | None = Field(
152
- # Timezone-aware local "now" so the value injected into the system prompt
153
- # carries a UTC offset instead of an ambiguous naive local time (#3438).
153
+ # Timezone-aware local "now"; get_formatted_datetime renders it to the
154
+ # minute for the prompt.
154
155
  default_factory=lambda: datetime.now().astimezone(),
155
156
  description=(
156
157
  "Current date and time information to provide to the agent. "
@@ -265,13 +266,17 @@ class AgentContext(BaseModel):
265
266
 
266
267
  Returns:
267
268
  Formatted datetime string, or None if current_datetime is not set.
268
- If current_datetime is a datetime object, it's formatted as ISO 8601.
269
+ If current_datetime is a datetime object, it's formatted as
270
+ "YYYY-MM-DDTHH:MM" (no seconds, microseconds, or UTC offset).
269
271
  If current_datetime is already a string, it's returned as-is.
270
272
  """
271
273
  if self.current_datetime is None:
272
274
  return None
273
275
  if isinstance(self.current_datetime, datetime):
274
- return self.current_datetime.isoformat()
276
+ # Local wall-clock to the minute: drop seconds, microseconds, offset.
277
+ return self.current_datetime.replace(tzinfo=None).isoformat(
278
+ timespec="minutes"
279
+ )
275
280
  return self.current_datetime
276
281
 
277
282
  def _partition_skills(self) -> tuple[list[Skill], list[Skill]]:
@@ -80,6 +80,16 @@ class LLMSummarizingCondenser(RollingCondenser):
80
80
  )
81
81
  return self
82
82
 
83
+ @model_validator(mode="after")
84
+ def _disable_streaming_for_summary(self):
85
+ # Summaries are consumed whole with no on_token callback, which a
86
+ # streaming LLM requires. Disable streaming once so every summary path
87
+ # is covered. model_copy is non-mutating and shares usage_id/metrics,
88
+ # so summary tokens stay attributed to the conversation.
89
+ if self.llm.stream:
90
+ self.llm = self.llm.model_copy(update={"stream": False})
91
+ return self
92
+
83
93
  def handles_condensation_requests(self) -> bool:
84
94
  return True
85
95
 
@@ -0,0 +1,109 @@
1
+ """Named :class:`PromptRegistry` presets -- ready-to-use section compositions.
2
+
3
+ ``create_registry()`` selects a section composition over the same engine. The
4
+ ``"default"`` preset registers the static-tier sections in the exact order
5
+ ``agent/prompts/system_prompt.j2`` emitted them, so ``registry.build(ctx).static``
6
+ reproduces ``AgentBase.static_system_message``. The ``"planning"`` preset is a
7
+ distinct standalone composition (ported from ``system_prompt_planning.j2``) that
8
+ omits the default OpenHands sections. The dynamic-tier sections are **shared** --
9
+ datetime/repo/skills/suffix/secrets are preset-independent -- so a planning agent
10
+ with an ``agent_context`` still gets its dynamic block.
11
+ """
12
+
13
+ from enum import StrEnum
14
+ from typing import Final
15
+
16
+ from openhands.sdk.context.prompts.registry import PromptRegistry
17
+ from openhands.sdk.context.prompts.section import PromptSection
18
+ from openhands.sdk.context.prompts.sections.dynamic import (
19
+ AvailableSkillsSection,
20
+ CustomSecretsSection,
21
+ CustomSuffixSection,
22
+ DateTimeSection,
23
+ RepoContextSection,
24
+ )
25
+ from openhands.sdk.context.prompts.sections.planning import PlanningSection
26
+ from openhands.sdk.context.prompts.sections.static import (
27
+ BrowserSection,
28
+ CodeQualitySection,
29
+ EfficiencySection,
30
+ EnvironmentSetupSection,
31
+ ExternalServicesSection,
32
+ FileSystemSection,
33
+ MemorySection,
34
+ ModelSpecificSection,
35
+ ProblemSolvingSection,
36
+ ProcessManagementSection,
37
+ PullRequestsSection,
38
+ RoleSection,
39
+ SecurityRiskAssessmentSection,
40
+ SecuritySection,
41
+ SelfDocumentationSection,
42
+ SoulSection,
43
+ TroubleshootingSection,
44
+ VersionControlSection,
45
+ )
46
+
47
+
48
+ __all__ = ["PromptPreset", "create_registry"]
49
+
50
+
51
+ class PromptPreset(StrEnum):
52
+ """Names a :func:`create_registry` section composition."""
53
+
54
+ DEFAULT = "default"
55
+ PLANNING = "planning"
56
+
57
+
58
+ _DEFAULT_STATIC_SECTIONS: Final[tuple[PromptSection, ...]] = (
59
+ SoulSection(),
60
+ RoleSection(),
61
+ MemorySection(),
62
+ EfficiencySection(),
63
+ FileSystemSection(),
64
+ CodeQualitySection(),
65
+ VersionControlSection(),
66
+ PullRequestsSection(),
67
+ ProblemSolvingSection(),
68
+ SelfDocumentationSection(),
69
+ SecuritySection(), # guard: security_policy_filename set
70
+ SecurityRiskAssessmentSection(), # guard: llm_security_analyzer
71
+ BrowserSection(), # guard: ctx.enable_browser
72
+ ExternalServicesSection(),
73
+ EnvironmentSetupSection(),
74
+ TroubleshootingSection(),
75
+ ProcessManagementSection(),
76
+ ModelSpecificSection(), # guard: model_family resolved
77
+ )
78
+
79
+ _PLANNING_STATIC_SECTIONS: Final[tuple[PromptSection, ...]] = (PlanningSection(),)
80
+
81
+ _DYNAMIC_SECTIONS: Final[tuple[PromptSection, ...]] = (
82
+ DateTimeSection(),
83
+ RepoContextSection(), # guard: gated repo skills present
84
+ AvailableSkillsSection(), # guard: available_skills_prompt
85
+ CustomSuffixSection(), # guard: system_message_suffix
86
+ CustomSecretsSection(), # guard: secret_infos present
87
+ )
88
+
89
+
90
+ def create_registry(preset: PromptPreset = PromptPreset.DEFAULT) -> PromptRegistry:
91
+ """Build the section registry for ``preset``.
92
+
93
+ ``DEFAULT`` is the standard OpenHands composition; ``PLANNING`` is the read-only
94
+ analysis composition (no ``<SECURITY>``/``<SOUL>``/``<MEMORY>`` ...). Both share
95
+ the dynamic tier. Sections are stateless, so the per-preset sequences are reused
96
+ across calls.
97
+ """
98
+ match preset:
99
+ case PromptPreset.PLANNING:
100
+ static_sections = _PLANNING_STATIC_SECTIONS
101
+ case PromptPreset.DEFAULT:
102
+ static_sections = _DEFAULT_STATIC_SECTIONS
103
+ case _:
104
+ raise ValueError(f"Unknown prompt preset: {preset!r}")
105
+
106
+ r = PromptRegistry()
107
+ for section in (*static_sections, *_DYNAMIC_SECTIONS):
108
+ r.register(section)
109
+ return r
@@ -3,7 +3,8 @@
3
3
  Sections are pure, synchronous functions of a :class:`PromptContext` -- no Jinja
4
4
  environment, no filesystem loader, no ``render_template`` bootstrap -- so each one
5
5
  unit-tests in isolation. The static tier (ported from ``system_prompt.j2``) lives
6
- in :mod:`.static`.
6
+ in :mod:`.static`; the standalone planning composition (ported from
7
+ ``system_prompt_planning.j2``) in :mod:`.planning`.
7
8
  """
8
9
 
9
10
  from openhands.sdk.context.prompts.sections.dynamic import (
@@ -13,6 +14,7 @@ from openhands.sdk.context.prompts.sections.dynamic import (
13
14
  DateTimeSection,
14
15
  RepoContextSection,
15
16
  )
17
+ from openhands.sdk.context.prompts.sections.planning import PlanningSection
16
18
  from openhands.sdk.context.prompts.sections.static import (
17
19
  BrowserSection,
18
20
  CodeQualitySection,
@@ -48,6 +50,7 @@ __all__ = [
48
50
  "FileSystemSection",
49
51
  "MemorySection",
50
52
  "ModelSpecificSection",
53
+ "PlanningSection",
51
54
  "ProblemSolvingSection",
52
55
  "ProcessManagementSection",
53
56
  "PullRequestsSection",
@@ -1,3 +1,34 @@
1
+ """The planning system prompt.
2
+
3
+ Unlike the default composition -- whose blocks each carry a guard and can be overridden
4
+ individually -- the planning prompt is a single standalone STATIC block with no
5
+ per-section guards, so it is one section. The only substitution is ``plan_structure``;
6
+ ``tests/sdk/context/prompts/test_planning_registry.py`` pins the output against a golden
7
+ snapshot. No ``_refine``: the text mentions no ``bash``/``terminal`` (its ``<EFFICIENCY>``
8
+ says ``glob and grep``), so the Windows shell substitution is a no-op.
9
+ """
10
+
11
+ # The body is verbatim long-form prompt text; wrapping a line would change the rendered
12
+ # bytes, so line-length (E501) is disabled for this whole file.
13
+ # ruff: noqa: E501
14
+
15
+ from openhands.sdk.context.prompts.section import CacheTier, PromptContext
16
+
17
+
18
+ __all__ = ["PlanningSection"]
19
+
20
+
21
+ class PlanningSection:
22
+ """The full planning system prompt as one STATIC block.
23
+
24
+ The only substitution is ``plan_structure`` (an empty value reproduces the
25
+ template's ``<PLAN_STRUCTURE>\\n\\n</PLAN_STRUCTURE>`` output verbatim).
26
+ """
27
+
28
+ name = "planning"
29
+ cache_tier = CacheTier.STATIC
30
+
31
+ _BODY = """\
1
32
  You are a Planning Agent that analyzes codebases and helps the user make a detailed plan for their requested changes.
2
33
 
3
34
  <ROLE>
@@ -92,5 +123,12 @@ Follow this enhanced planning workflow to create well-researched, user-aligned p
92
123
  </PLAN_SCOPE>
93
124
 
94
125
  <PLAN_STRUCTURE>
95
- {{plan_structure}}
96
- </PLAN_STRUCTURE>
126
+ {plan_structure}
127
+ </PLAN_STRUCTURE>"""
128
+
129
+ def guard(self, ctx: PromptContext) -> bool: # noqa: ARG002
130
+ return True
131
+
132
+ def render(self, ctx: PromptContext) -> str | None:
133
+ plan_structure = str(ctx.template_kwargs.get("plan_structure", ""))
134
+ return self._BODY.replace("{plan_structure}", plan_structure)
@@ -18,6 +18,7 @@ from openhands.sdk.observability.laminar import (
18
18
  RootSpan,
19
19
  end_root_span,
20
20
  should_enable_observability,
21
+ start_child_span,
21
22
  start_root_span,
22
23
  )
23
24
  from openhands.sdk.security.analyzer import SecurityAnalyzerBase
@@ -139,6 +140,7 @@ class BaseConversation(ABC):
139
140
  def _start_observability_span(
140
141
  self,
141
142
  session_id: str,
143
+ span_name: str = "conversation",
142
144
  user_id: str | None = None,
143
145
  metadata: dict[str, TraceMetadataValue] | None = None,
144
146
  tags: list[str] | None = None,
@@ -148,6 +150,7 @@ class BaseConversation(ABC):
148
150
 
149
151
  Args:
150
152
  session_id: The session ID to associate with the trace
153
+ span_name: Optional child span name to emit under the conversation root.
151
154
  user_id: Optional user ID to associate with the trace
152
155
  metadata: Optional trace-level metadata to attach to observability backends
153
156
  tags: Optional span tags to attach to the conversation root span
@@ -166,12 +169,15 @@ class BaseConversation(ABC):
166
169
  tags=tags,
167
170
  attributes=_conversation_tag_attributes(conversation_tags),
168
171
  )
172
+ if span_name != "conversation":
173
+ start_child_span(self._observability_root_span, span_name, tags=tags)
169
174
 
170
175
  def _end_observability_span(self) -> None:
171
176
  """End the observability span if it hasn't been ended already."""
172
177
  if self._span_ended:
173
178
  return
174
- end_root_span(self._observability_root_span)
179
+ if self._observability_root_span is not None:
180
+ end_root_span(self._observability_root_span)
175
181
  self._observability_root_span = None
176
182
  self._span_ended = True
177
183
 
@@ -378,6 +384,19 @@ class BaseConversation(ABC):
378
384
  """
379
385
  ...
380
386
 
387
+ def load_plugin(self, plugin_ref: str) -> None:
388
+ """Load a plugin from a registered marketplace.
389
+
390
+ Implementations that support marketplace-registered plugins resolve the
391
+ reference against the conversation agent's registered marketplaces and
392
+ merge the plugin's skills, hooks, and MCP configuration into the agent.
393
+
394
+ Args:
395
+ plugin_ref: Plugin reference, either ``plugin-name`` or
396
+ ``plugin-name@marketplace-name``.
397
+ """
398
+ raise NotImplementedError("This conversation does not support loading plugins")
399
+
381
400
  @abstractmethod
382
401
  def fork(
383
402
  self,