openhands-sdk 1.55.0__tar.gz → 1.56.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 (327) hide show
  1. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/PKG-INFO +2 -2
  2. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/base.py +4 -21
  3. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/impl/local_conversation.py +33 -5
  4. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/impl/remote_conversation.py +70 -3
  5. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/request.py +48 -1
  6. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/git_changes.py +31 -6
  7. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/llm_profile_store.py +7 -1
  8. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/meta_profile_store.py +4 -5
  9. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/unverified_models.py +16 -3
  10. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/marketplace/registry.py +20 -1
  11. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/marketplace/types.py +55 -0
  12. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/__init__.py +8 -0
  13. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/fetch.py +34 -1
  14. openhands_sdk-1.56.0/openhands/sdk/plugin/paths.py +17 -0
  15. openhands_sdk-1.56.0/openhands/sdk/plugin/strict_fetch.py +504 -0
  16. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/__init__.py +4 -0
  17. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/agent_profile.py +35 -8
  18. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/profile_refs.py +7 -2
  19. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/model.py +30 -68
  20. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/installed.py +6 -1
  21. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/defaults.py +0 -18
  22. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/redact.py +3 -0
  23. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands_sdk.egg-info/PKG-INFO +2 -2
  24. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands_sdk.egg-info/SOURCES.txt +4 -0
  25. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands_sdk.egg-info/requires.txt +1 -1
  26. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/pyproject.toml +2 -2
  27. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/__init__.py +0 -0
  28. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/__init__.py +0 -0
  29. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_agent.py +0 -0
  30. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  31. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_file_credentials.py +0 -0
  32. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  33. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_models.py +0 -0
  34. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  35. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/agent.py +0 -0
  36. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  37. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  38. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  39. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/stream_context.py +0 -0
  40. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/agent/utils.py +0 -0
  41. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/automation/__init__.py +0 -0
  42. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/banner.py +0 -0
  43. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/__init__.py +0 -0
  44. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/agent_context.py +0 -0
  45. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  46. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/base.py +0 -0
  47. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  48. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  49. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  50. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  51. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  52. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/condenser/utils.py +0 -0
  53. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/memory.py +0 -0
  54. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  55. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/presets.py +0 -0
  56. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  57. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/registry.py +0 -0
  58. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/section.py +0 -0
  59. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  60. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  61. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  62. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
  63. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  64. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  65. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/__init__.py +0 -0
  66. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  67. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  68. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/base.py +0 -0
  69. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  70. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  71. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  72. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  73. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/context/view/view.py +0 -0
  74. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/__init__.py +0 -0
  75. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/base.py +0 -0
  76. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/cancellation.py +0 -0
  77. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/conversation.py +0 -0
  78. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  79. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/event_store.py +0 -0
  80. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  81. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/exceptions.py +0 -0
  82. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  83. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  84. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/goal/controller.py +0 -0
  85. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  86. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  87. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  88. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  89. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/message_request.py +0 -0
  90. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  91. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  92. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/response_utils.py +0 -0
  93. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  94. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/state.py +0 -0
  95. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  96. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/title_utils.py +0 -0
  97. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/types.py +0 -0
  98. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  99. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  100. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  101. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/credential.py +0 -0
  102. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/__init__.py +0 -0
  103. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/base.py +0 -0
  104. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  105. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  106. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  107. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  108. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  109. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  110. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  111. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  112. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  113. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/critic/result.py +0 -0
  114. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/__init__.py +0 -0
  115. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  116. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/base.py +0 -0
  117. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/condenser.py +0 -0
  118. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/conversation_error.py +0 -0
  119. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/conversation_state.py +0 -0
  120. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/error_classification.py +0 -0
  121. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/hook_execution.py +0 -0
  122. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  123. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  124. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  125. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  126. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  127. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  128. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  129. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/resume_transcript.py +0 -0
  130. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/security.py +0 -0
  131. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/streaming_delta.py +0 -0
  132. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/token.py +0 -0
  133. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/types.py +0 -0
  134. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/event/user_action.py +0 -0
  135. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/__init__.py +0 -0
  136. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/fetch.py +0 -0
  137. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  138. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/info.py +0 -0
  139. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  140. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  141. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  142. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  143. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/cached_repo.py +0 -0
  144. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/exceptions.py +0 -0
  145. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/git_commits.py +0 -0
  146. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/git_diff.py +0 -0
  147. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/models.py +0 -0
  148. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/git/utils.py +0 -0
  149. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/__init__.py +0 -0
  150. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/config.py +0 -0
  151. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  152. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/executor.py +0 -0
  153. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/manager.py +0 -0
  154. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/hooks/types.py +0 -0
  155. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/io/__init__.py +0 -0
  156. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/io/base.py +0 -0
  157. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/io/cache.py +0 -0
  158. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/io/local.py +0 -0
  159. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/io/memory.py +0 -0
  160. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/launch/__init__.py +0 -0
  161. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/launch/errors.py +0 -0
  162. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/launch/finalize.py +0 -0
  163. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/launch/preview.py +0 -0
  164. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/launch/resolve.py +0 -0
  165. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/__init__.py +0 -0
  166. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  167. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/_response_stream.py +0 -0
  168. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  169. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  170. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  171. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/auth/openai.py +0 -0
  172. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/call_context.py +0 -0
  173. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  174. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  175. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  176. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  177. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  178. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  179. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/llm.py +0 -0
  180. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/llm_registry.py +0 -0
  181. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/llm_response.py +0 -0
  182. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/message.py +0 -0
  183. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  184. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  185. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  186. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/options/__init__.py +0 -0
  187. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  188. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/options/common.py +0 -0
  189. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  190. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  191. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/router/__init__.py +0 -0
  192. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/router/base.py +0 -0
  193. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  194. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  195. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/streaming.py +0 -0
  196. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  197. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  198. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  199. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  200. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/model_features.py +0 -0
  201. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  202. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  203. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
  204. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  205. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  206. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  207. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  208. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  209. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  210. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/verified_models.py +0 -0
  211. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  212. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/logger/__init__.py +0 -0
  213. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/logger/logger.py +0 -0
  214. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/logger/rolling.py +0 -0
  215. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/marketplace/__init__.py +0 -0
  216. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/marketplace/registration.py +0 -0
  217. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/__init__.py +0 -0
  218. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/client.py +0 -0
  219. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/config.py +0 -0
  220. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/definition.py +0 -0
  221. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/exceptions.py +0 -0
  222. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/oauth.py +0 -0
  223. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/tool.py +0 -0
  224. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/mcp/utils.py +0 -0
  225. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/observability/__init__.py +0 -0
  226. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/observability/laminar.py +0 -0
  227. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/observability/utils.py +0 -0
  228. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/discovery.py +0 -0
  229. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  230. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
  231. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  232. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/base.py +0 -0
  233. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  234. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  235. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  236. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/installed.py +0 -0
  237. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/loader.py +0 -0
  238. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/plugin.py +0 -0
  239. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/source.py +0 -0
  240. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/plugin/types.py +0 -0
  241. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/agent_profile_store.py +0 -0
  242. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/resolver.py +0 -0
  243. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/profiles/seed.py +0 -0
  244. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/py.typed +0 -0
  245. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/secret/__init__.py +0 -0
  246. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/secret/secrets.py +0 -0
  247. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/__init__.py +0 -0
  248. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/_shell_ast.py +0 -0
  249. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/analyzer.py +0 -0
  250. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  251. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  252. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  253. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  254. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
  255. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  256. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/ensemble.py +0 -0
  257. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  258. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  259. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  260. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  261. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/risk.py +0 -0
  262. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/shell_parser.py +0 -0
  263. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  264. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  265. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/__init__.py +0 -0
  266. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
  267. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/acp_providers.py +0 -0
  268. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/api_models.py +0 -0
  269. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/settings/metadata.py +0 -0
  270. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/__init__.py +0 -0
  271. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/exceptions.py +0 -0
  272. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/execute.py +0 -0
  273. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/fetch.py +0 -0
  274. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/skill.py +0 -0
  275. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/trigger.py +0 -0
  276. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/types.py +0 -0
  277. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/skills/utils.py +0 -0
  278. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/subagent/__init__.py +0 -0
  279. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/subagent/load.py +0 -0
  280. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/subagent/registry.py +0 -0
  281. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/subagent/schema.py +0 -0
  282. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/subagent/scope.py +0 -0
  283. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/testing/__init__.py +0 -0
  284. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/testing/test_llm.py +0 -0
  285. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/__init__.py +0 -0
  286. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  287. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  288. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  289. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  290. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  291. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/think.py +0 -0
  292. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  293. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/client_tool.py +0 -0
  294. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/registry.py +0 -0
  295. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/schema.py +0 -0
  296. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/spec.py +0 -0
  297. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/tool/tool.py +0 -0
  298. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/__init__.py +0 -0
  299. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/async_executor.py +0 -0
  300. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/async_utils.py +0 -0
  301. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/cipher.py +0 -0
  302. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/command.py +0 -0
  303. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/datetime.py +0 -0
  304. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/deprecation.py +0 -0
  305. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/files.py +0 -0
  306. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/github.py +0 -0
  307. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/json.py +0 -0
  308. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/masking.py +0 -0
  309. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/models.py +0 -0
  310. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/paging.py +0 -0
  311. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/path.py +0 -0
  312. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  313. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/truncate.py +0 -0
  314. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/utils/visualize.py +0 -0
  315. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/__init__.py +0 -0
  316. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/base.py +0 -0
  317. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/local.py +0 -0
  318. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/models.py +0 -0
  319. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  320. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  321. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/remote/base.py +0 -0
  322. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  323. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/repo.py +0 -0
  324. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands/sdk/workspace/workspace.py +0 -0
  325. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  326. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  327. {openhands_sdk-1.55.0 → openhands_sdk-1.56.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.55.0
3
+ Version: 1.56.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
@@ -15,7 +15,7 @@ Requires-Dist: filelock>=3.20.1
15
15
  Requires-Dist: httpx[socks]>=0.28.1
16
16
  Requires-Dist: jsonschema>=4.23.0
17
17
  Requires-Dist: joserfc>=1.7.5
18
- Requires-Dist: litellm>=1.93.0
18
+ Requires-Dist: litellm<1.106,>=1.93.0
19
19
  Requires-Dist: pillow>=12.3.0
20
20
  Requires-Dist: pydantic>=2.13.5
21
21
  Requires-Dist: python-frontmatter>=1.3.0
@@ -33,7 +33,6 @@ from openhands.sdk.mcp.client import MCPClient
33
33
  from openhands.sdk.mcp.config import MCPServer
34
34
  from openhands.sdk.mcp.tool import MCPToolDefinition, MCPToolExecutor
35
35
  from openhands.sdk.tool import (
36
- BROWSER_TOOL_NAME,
37
36
  BUILT_IN_TOOL_CLASSES,
38
37
  BUILT_IN_TOOLS,
39
38
  Tool,
@@ -45,7 +44,6 @@ from openhands.sdk.tool.builtins.vision_inspect import (
45
44
  VisionInspectTool,
46
45
  has_vision_profile_available,
47
46
  )
48
- from openhands.sdk.utils.deprecation import warn_deprecated
49
47
  from openhands.sdk.utils.models import DiscriminatedUnionMixin
50
48
  from openhands.sdk.utils.path import get_user_persistence_dir
51
49
 
@@ -268,21 +266,11 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
268
266
 
269
267
  @field_validator("system_prompt_kwargs")
270
268
  @classmethod
271
- def _warn_deprecated_enable_browser(
269
+ def _drop_retired_enable_browser(
272
270
  cls, value: dict[str, object]
273
271
  ) -> dict[str, object]:
274
- if "enable_browser" in value:
275
- warn_deprecated(
276
- "system_prompt_kwargs['enable_browser']",
277
- deprecated_in="1.51.0",
278
- removed_in="1.56.0",
279
- details=(
280
- "Browser guidance now comes from the loaded browser tools; "
281
- "only custom Jinja templates still read this kwarg."
282
- ),
283
- stacklevel=5,
284
- )
285
- return value
272
+ """Old saved conversations may still carry this retired prompt flag."""
273
+ return {key: item for key, item in value.items() if key != "enable_browser"}
286
274
 
287
275
  condenser: CondenserBase | None = Field(
288
276
  default=None,
@@ -378,15 +366,10 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
378
366
  # own Jinja template; everything else (incl. custom policies) uses the registry.
379
367
  preset = self._prompt_preset
380
368
  if preset is None:
381
- # Deprecated kwarg, kept until 1.56.0 for templates that branch on it.
382
- template_kwargs = {
383
- "enable_browser": any(t.name == BROWSER_TOOL_NAME for t in self.tools),
384
- **self._resolved_template_kwargs(),
385
- }
386
369
  return render_template(
387
370
  prompt_dir=self.prompt_dir,
388
371
  template_name=self.system_prompt_filename,
389
- **template_kwargs,
372
+ **self._resolved_template_kwargs(),
390
373
  )
391
374
 
392
375
  return create_registry(preset).build(self._build_prompt_context()).static
@@ -177,6 +177,16 @@ def _copy_event_for_fork(event: Event) -> Event:
177
177
  return Event.model_validate_json(event.model_dump_json(exclude_none=True))
178
178
 
179
179
 
180
+ def _same_llm_config(a: LLM, b: LLM) -> bool:
181
+ """Return whether two LLMs serialize to the same config, secrets included."""
182
+ if a is b:
183
+ return True
184
+ context = {"expose_secrets": True}
185
+ return a.model_dump(mode="json", context=context) == b.model_dump(
186
+ mode="json", context=context
187
+ )
188
+
189
+
180
190
  class LocalConversation(BaseConversation):
181
191
  agent: AgentBase
182
192
  workspace: LocalWorkspace
@@ -1664,18 +1674,31 @@ class LocalConversation(BaseConversation):
1664
1674
  """Swap the agent's LLM to the given object.
1665
1675
 
1666
1676
  The caller owns ``llm.usage_id``; it is the registry key. If an
1667
- entry with that key already exists, the cached LLM is reused and
1668
- the passed ``llm`` is dropped — matching the rest of the
1669
- registry's "first-write-wins" contract.
1677
+ entry with that key already exists and has the same config, the
1678
+ cached LLM is reused. If its config differs (e.g. ``default``, which
1679
+ agent initialization registers, posted with another model), the
1680
+ passed ``llm`` replaces the entry and takes over the metrics already
1681
+ recorded under that ``usage_id``.
1670
1682
 
1671
1683
  Args:
1672
1684
  llm: LLM to install on the agent.
1673
1685
  """
1686
+ usage_id = llm.usage_id
1674
1687
  try:
1675
- new_llm = self.llm_registry.get(llm.usage_id)
1688
+ registered = self.llm_registry.get(usage_id)
1676
1689
  except KeyError:
1690
+ registered = None
1691
+ if registered is not None and _same_llm_config(registered, llm):
1692
+ new_llm = registered
1693
+ else:
1677
1694
  new_llm = create_subscription_llm_from_config(llm)
1695
+ if registered is not None:
1696
+ self.llm_registry.remove(usage_id)
1678
1697
  self.llm_registry.add(new_llm)
1698
+ if registered is not None:
1699
+ # Keep one metrics object per usage_id: ConversationStats
1700
+ # tracks the registered LLM's, so accrued usage is retained.
1701
+ new_llm.restore_metrics(registered.metrics)
1679
1702
  # A switch_llm tool runs on a worker thread while run()/arun() holds the
1680
1703
  # state lock across the agent step on another thread, blocked awaiting
1681
1704
  # this very tool. Re-acquiring _state here would deadlock, so skip it:
@@ -1760,7 +1783,8 @@ class LocalConversation(BaseConversation):
1760
1783
 
1761
1784
  Raises:
1762
1785
  ValueError: If the conversation's agent is not an :class:`ACPAgent`,
1763
- or (for a live switch) the provider does not support runtime
1786
+ if ``model`` is empty or whitespace-only (live or not), or
1787
+ (for a live switch) the provider does not support runtime
1764
1788
  model switching, or the ACP server rejects the switch.
1765
1789
  TimeoutError: If a live switch's ``session/set_model`` round-trip
1766
1790
  exceeds ``acp_prompt_timeout`` seconds.
@@ -1769,6 +1793,10 @@ class LocalConversation(BaseConversation):
1769
1793
  raise ValueError(
1770
1794
  "switch_acp_model is only supported for ACP conversations."
1771
1795
  )
1796
+ # Check here, not only in set_acp_model: the pre-run path below skips
1797
+ # that call and would persist a blank model unchecked.
1798
+ if not model or not model.strip():
1799
+ raise ValueError("model must be a non-empty string")
1772
1800
  with self._state:
1773
1801
  # With a live session, perform the protocol switch first; if it
1774
1802
  # fails we leave the persisted state untouched. Without one (pre
@@ -13,6 +13,7 @@ from urllib.parse import urlparse
13
13
 
14
14
  import httpx
15
15
  import websockets
16
+ from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices
16
17
  from websockets.exceptions import ConnectionClosed
17
18
 
18
19
  from openhands.sdk.agent.base import AgentBase
@@ -34,6 +35,7 @@ from openhands.sdk.conversation.title_utils import generate_conversation_title
34
35
  from openhands.sdk.conversation.types import (
35
36
  ConversationCallbackType,
36
37
  ConversationID,
38
+ ConversationTokenCallbackType,
37
39
  StuckDetectionThresholds,
38
40
  TraceMetadataValue,
39
41
  )
@@ -49,6 +51,7 @@ from openhands.sdk.event.conversation_state import (
49
51
  ConversationStateUpdateEvent,
50
52
  )
51
53
  from openhands.sdk.event.llm_completion_log import LLMCompletionLogEvent
54
+ from openhands.sdk.event.streaming_delta import StreamingDeltaEvent
52
55
  from openhands.sdk.event.types import EventID
53
56
  from openhands.sdk.hooks import HookConfig
54
57
  from openhands.sdk.llm import LLM, Message, TextContent
@@ -92,6 +95,26 @@ def _validate_remote_agent(agent_data: dict) -> AgentBase:
92
95
  return AgentBase.model_validate(agent_data)
93
96
 
94
97
 
98
+ def _token_delta_callback(
99
+ token_callbacks: list[ConversationTokenCallbackType],
100
+ ) -> ConversationCallbackType:
101
+ """Hand each streamed delta from the events socket to the token callbacks."""
102
+ on_token = BaseConversation.compose_callbacks(token_callbacks)
103
+
104
+ def callback(event: Event) -> None:
105
+ if isinstance(event, StreamingDeltaEvent):
106
+ delta = Delta(
107
+ content=event.content, reasoning_content=event.reasoning_content
108
+ )
109
+ on_token(
110
+ ModelResponseStream(
111
+ id=event.id, choices=[StreamingChoices(index=0, delta=delta)]
112
+ )
113
+ )
114
+
115
+ return callback
116
+
117
+
95
118
  def _restore_tool_registrations(tool_module_qualnames: Mapping[str, str]) -> None:
96
119
  """Import the tool modules persisted with a remote conversation."""
97
120
  for tool_name, module_qualname in tool_module_qualnames.items():
@@ -493,9 +516,16 @@ class RemoteEventsList(EventsListBase):
493
516
  return len(self._cached_events) - 1
494
517
 
495
518
  def create_default_callback(self) -> ConversationCallbackType:
496
- """Create a default callback that adds events to this list."""
519
+ """Create a default callback that adds events to this list.
520
+
521
+ Streamed ``StreamingDeltaEvent`` frames are transient: the server never
522
+ stores them, so they reach the other callbacks but stay out of this
523
+ list, which keeps matching the server's event log.
524
+ """
497
525
 
498
526
  def callback(event: Event) -> None:
527
+ if isinstance(event, StreamingDeltaEvent):
528
+ return
499
529
  self.add_event(event)
500
530
 
501
531
  return callback
@@ -746,6 +776,7 @@ class RemoteConversation(BaseConversation):
746
776
  observability_tags: list[str] | None = None,
747
777
  observability_span_name: str = "conversation",
748
778
  observability_parent_span_context: str | None = None,
779
+ token_callbacks: list[ConversationTokenCallbackType] | None = None,
749
780
  **_: object,
750
781
  ) -> None:
751
782
  """Remote conversation proxy that talks to an agent server.
@@ -783,6 +814,8 @@ class RemoteConversation(BaseConversation):
783
814
  observability_tags: Optional root span tags for observability backends.
784
815
  observability_span_name: Optional child span name for observability
785
816
  backends. The root span remains named "conversation".
817
+ token_callbacks: Optional callbacks invoked with each streamed token
818
+ delta the server sends (agent LLM with ``stream=True``).
786
819
  """
787
820
  observability_metadata = merge_observability_metadata(observability_metadata)
788
821
  observability_parent_span_context = (
@@ -920,6 +953,7 @@ class RemoteConversation(BaseConversation):
920
953
  workspace=workspace,
921
954
  conversation_id=conversation_id,
922
955
  callbacks=callbacks,
956
+ token_callbacks=token_callbacks,
923
957
  max_iteration_per_run=max_iteration_per_run,
924
958
  client_tools=[*(client_tools or []), *attached_client_tools],
925
959
  visualizer=visualizer,
@@ -1060,10 +1094,13 @@ class RemoteConversation(BaseConversation):
1060
1094
  visualizer: (
1061
1095
  type[ConversationVisualizerBase] | ConversationVisualizerBase | None
1062
1096
  ),
1097
+ token_callbacks: list[ConversationTokenCallbackType] | None = None,
1063
1098
  ) -> None:
1064
1099
  super().__init__() # Initialize base class with span tracking
1065
1100
  self.agent = agent
1066
1101
  self._callbacks = callbacks or []
1102
+ if token_callbacks:
1103
+ self._callbacks.append(_token_delta_callback(token_callbacks))
1067
1104
  self.max_iteration_per_run = max_iteration_per_run
1068
1105
  self.workspace = workspace
1069
1106
  self._client = workspace.client
@@ -1166,13 +1203,40 @@ class RemoteConversation(BaseConversation):
1166
1203
  all_callbacks = self._callbacks + [run_complete_callback]
1167
1204
  composed_callback = BaseConversation.compose_callbacks(all_callbacks)
1168
1205
 
1206
+ # Track which events the callbacks have seen, so that events the
1207
+ # server recorded while the socket was reconnecting (and that only
1208
+ # reached the cache through REST reconciliation) can be replayed to
1209
+ # them once, without the live socket delivering them a second time.
1210
+ delivered_ids: set[str] = set()
1211
+ replayed_ids: set[str] = set()
1212
+
1213
+ def socket_callback(event: Event) -> None:
1214
+ if event.id in replayed_ids:
1215
+ replayed_ids.discard(event.id)
1216
+ return
1217
+ delivered_ids.add(event.id)
1218
+ composed_callback(event)
1219
+
1220
+ def replay_missed_events() -> None:
1221
+ self._state.events.reconcile()
1222
+ replayed_ids.clear()
1223
+ for event in self._state.events[:]:
1224
+ if event.id in delivered_ids:
1225
+ continue
1226
+ delivered_ids.add(event.id)
1227
+ replayed_ids.add(event.id)
1228
+ try:
1229
+ composed_callback(event)
1230
+ except Exception:
1231
+ logger.exception("ws_replay_callback_error", stack_info=True)
1232
+
1169
1233
  # Initialize WebSocket client for callbacks
1170
1234
  self._ws_client = WebSocketCallbackClient(
1171
1235
  host=self.workspace.host,
1172
1236
  conversation_id=str(self._id),
1173
- callback=composed_callback,
1237
+ callback=socket_callback,
1174
1238
  api_key=self.workspace.api_key,
1175
- on_reconnect=self._state.events.reconcile,
1239
+ on_reconnect=replay_missed_events,
1176
1240
  )
1177
1241
  self._ws_client.start()
1178
1242
 
@@ -1210,6 +1274,9 @@ class RemoteConversation(BaseConversation):
1210
1274
  # were emitted between the initial REST sync and WebSocket subscription.
1211
1275
  # This is the "reconciliation" part of the subscription handshake.
1212
1276
  self._state.events.reconcile()
1277
+ # Callbacks are not replayed history: only events recorded after this
1278
+ # point and missed by the socket are replayed on reconnect.
1279
+ delivered_ids.update(event.id for event in self._state.events[:])
1213
1280
 
1214
1281
  def _create_llm_completion_log_callback(self) -> ConversationCallbackType:
1215
1282
  """Create a callback that writes LLM completion logs to client filesystem."""
@@ -8,7 +8,7 @@ agent-server.
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
- from typing import Annotated, Any, cast
11
+ from typing import Annotated, Any, Literal, cast
12
12
  from uuid import UUID
13
13
 
14
14
  from pydantic import (
@@ -65,6 +65,53 @@ ACPEnabledAgent = Annotated[
65
65
  # ---------------------------------------------------------------------------
66
66
 
67
67
 
68
+ class StartChildConversationRequest(BaseModel):
69
+ """Payload to start a child conversation on the same backend as its parent.
70
+
71
+ Sent by the ``start_child_conversation`` tool to a child-launch endpoint:
72
+ the agent-server's ``POST /api/conversations/{id}/children`` by default, or
73
+ an equivalent launcher supplied by the backend hosting the parent.
74
+ """
75
+
76
+ task: str = Field(
77
+ min_length=1,
78
+ description=(
79
+ "Self-contained brief delivered to the child as its first user message."
80
+ ),
81
+ )
82
+ title: str | None = Field(
83
+ default=None, description="Optional title for the child conversation."
84
+ )
85
+ isolation: Literal["worktree", "shared"] = Field(
86
+ default="worktree",
87
+ description=(
88
+ "Workspace isolation for the child. 'worktree' runs it in a git "
89
+ "worktree carved from the parent's workspace when that workspace is a "
90
+ "git repository; 'shared' runs it in the parent's directory. Backends "
91
+ "that manage workspaces themselves may ignore this."
92
+ ),
93
+ )
94
+
95
+
96
+ class StartChildConversationResponse(BaseModel):
97
+ """Result of a child conversation launch, returned to the parent agent."""
98
+
99
+ conversation_id: UUID
100
+ parent_conversation_id: UUID
101
+ status: str = Field(description="Initial launch status reported by the backend.")
102
+ title: str | None = None
103
+ url: str | None = Field(
104
+ default=None,
105
+ description="Link to the child conversation, when the backend knows it.",
106
+ )
107
+ workspace: str | None = Field(
108
+ default=None, description="Working directory of the child, when known."
109
+ )
110
+ isolation: Literal["worktree", "shared"] | None = Field(
111
+ default=None, description="Workspace isolation actually applied, when known."
112
+ )
113
+
114
+
68
115
  class AgentLaunchAdditions(BaseModel):
69
116
  """Add deployment context after agent resolution."""
70
117
 
@@ -9,7 +9,11 @@ import logging
9
9
  import os
10
10
  from pathlib import Path
11
11
 
12
- from openhands.sdk.git.exceptions import GitCommandError, GitError
12
+ from openhands.sdk.git.exceptions import (
13
+ GitCommandError,
14
+ GitError,
15
+ GitRepositoryError,
16
+ )
13
17
  from openhands.sdk.git.models import GitChange, GitChangeStatus
14
18
  from openhands.sdk.git.utils import (
15
19
  get_valid_ref,
@@ -154,7 +158,9 @@ def get_changes_in_repo(
154
158
  vs the latest commit) instead.
155
159
 
156
160
  Args:
157
- repo_dir: Path to the git repository
161
+ repo_dir: Path to the git repository, or to a directory inside one.
162
+ Only changes under this directory are listed, with paths relative
163
+ to it.
158
164
  ref: Optional explicit ref to compare against (e.g. ``"HEAD"`` or a
159
165
  commit hash). When ``None``, behaves as before and compares
160
166
  against the upstream/default branch.
@@ -178,10 +184,14 @@ def get_changes_in_repo(
178
184
  logger.warning(f"No valid git reference found for {validated_repo}")
179
185
  return []
180
186
 
181
- # Get changed files using secure git command
187
+ # Get changed files using secure git command. ``--relative`` limits the
188
+ # diff to ``repo_dir`` and prints paths relative to it, the same base as
189
+ # ``git ls-files --others`` below, so a subdirectory of a repository does
190
+ # not mix repository-relative and subdirectory-relative paths.
182
191
  try:
183
192
  changed_files_output = run_git_command(
184
- ["git", "--no-pager", "diff", "--name-status", ref], validated_repo
193
+ ["git", "--no-pager", "diff", "--name-status", "--relative", ref],
194
+ validated_repo,
185
195
  )
186
196
  changed_files = (
187
197
  changed_files_output.splitlines() if changed_files_output else []
@@ -221,8 +231,17 @@ def get_git_changes(cwd: str | Path, ref: str | None = None) -> list[GitChange]:
221
231
  for f in glob.glob("./*/.git", root_dir=cwd, recursive=True)
222
232
  }
223
233
 
224
- # First try the workspace directory
225
- changes = get_changes_in_repo(cwd, ref=ref)
234
+ # First try the workspace directory. A directory that is not a repository
235
+ # itself but holds repositories below it (the ``{base}/{repo}`` layout of a
236
+ # repository-backed conversation) contributes no changes of its own.
237
+ base_error: GitRepositoryError | None = None
238
+ try:
239
+ changes = get_changes_in_repo(cwd, ref=ref)
240
+ except GitRepositoryError as e:
241
+ if not git_dirs:
242
+ raise
243
+ base_error = e
244
+ changes = []
226
245
 
227
246
  # Filter out any changes which are inside one of the nested repositories.
228
247
  # This compares path ancestry rather than string prefixes: a nested
@@ -236,6 +255,7 @@ def get_git_changes(cwd: str | Path, ref: str | None = None) -> list[GitChange]:
236
255
  ]
237
256
 
238
257
  # Add changes from git directories
258
+ found_nested_repo = False
239
259
  for git_dir in git_dirs:
240
260
  try:
241
261
  git_dir_changes = get_changes_in_repo(str(Path(cwd, git_dir)), ref=ref)
@@ -244,6 +264,7 @@ def get_git_changes(cwd: str | Path, ref: str | None = None) -> list[GitChange]:
244
264
  f"Skipping nested git directory {git_dir}: not a valid repository"
245
265
  )
246
266
  continue
267
+ found_nested_repo = True
247
268
  for change in git_dir_changes:
248
269
  # Create a new GitChange with the updated path
249
270
  updated_change = GitChange(
@@ -252,6 +273,10 @@ def get_git_changes(cwd: str | Path, ref: str | None = None) -> list[GitChange]:
252
273
  )
253
274
  changes.append(updated_change)
254
275
 
276
+ # Neither the directory nor anything below it is a repository.
277
+ if base_error is not None and not found_nested_repo:
278
+ raise base_error
279
+
255
280
  changes.sort(key=lambda change: str(change.path))
256
281
 
257
282
  return changes
@@ -34,6 +34,12 @@ _LOCK_TIMEOUT_SECONDS: Final[float] = 30.0
34
34
  PROFILE_NAME_PATTERN: Final[str] = r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$"
35
35
  PROFILE_NAME_REGEX: Final[re.Pattern[str]] = re.compile(PROFILE_NAME_PATTERN)
36
36
 
37
+
38
+ def canonical_profile_name(name: str) -> str:
39
+ """Return the store key ``name`` addresses: ``foo.json`` is profile ``foo``."""
40
+ return name.removesuffix(".json")
41
+
42
+
37
43
  logger = get_logger(__name__)
38
44
 
39
45
 
@@ -163,7 +169,7 @@ class LLMProfileStore:
163
169
  Raises:
164
170
  ValueError: If name does not match the allowed pattern.
165
171
  """
166
- clean_name = name.removesuffix(".json")
172
+ clean_name = canonical_profile_name(name)
167
173
  if not PROFILE_NAME_REGEX.match(clean_name):
168
174
  raise ValueError(
169
175
  f"Invalid profile name: {name!r}. "
@@ -10,7 +10,6 @@ string — credentials/provider settings resolve through that store.
10
10
  from __future__ import annotations
11
11
 
12
12
  import json
13
- import os
14
13
  import re
15
14
  import tempfile
16
15
  from collections.abc import Iterator
@@ -23,6 +22,7 @@ from pydantic import BaseModel, Field, model_validator
23
22
 
24
23
  from openhands.sdk.llm.llm_profile_store import PROFILE_NAME_REGEX
25
24
  from openhands.sdk.logger import get_logger
25
+ from openhands.sdk.utils.path import get_user_persistence_dir
26
26
 
27
27
 
28
28
  _LOCK_TIMEOUT_SECONDS: Final[float] = 30.0
@@ -37,11 +37,10 @@ def default_meta_profile_dir() -> Path:
37
37
  persisted state. Honor that for bare ``MetaProfileStore()`` instances so
38
38
  runtime tools read the same meta-profiles exposed by the HTTP API. Without
39
39
  that env var, keep the standalone SDK default under ``~/.openhands``.
40
+ Resolved at call time through ``get_user_persistence_dir`` so ``~`` and
41
+ relative values land where the other stores put their files.
40
42
  """
41
- env_dir = os.environ.get("OH_PERSISTENCE_DIR")
42
- if env_dir:
43
- return Path(env_dir) / "meta-profiles"
44
- return Path.home() / ".openhands" / "meta-profiles"
43
+ return get_user_persistence_dir() / "meta-profiles"
45
44
 
46
45
 
47
46
  class MetaProfileLimitExceeded(Exception):
@@ -51,6 +51,19 @@ def _list_bedrock_foundation_models(
51
51
  return []
52
52
 
53
53
 
54
+ def _is_litellm_bedrock_model(model: str) -> bool:
55
+ """Whether LiteLLM serves ``model`` only through AWS Bedrock.
56
+
57
+ Many Bedrock ids carry no ``bedrock`` prefix (``deepseek.r1-v1:0``,
58
+ ``us.anthropic.claude-...``), so ask LiteLLM's model-cost map which provider
59
+ it files the id under (``bedrock``, ``bedrock_converse``, ``bedrock_mantle``).
60
+ """
61
+ if model.startswith("bedrock"):
62
+ return True
63
+ provider = litellm.model_cost.get(model, {}).get("litellm_provider")
64
+ return isinstance(provider, str) and provider.startswith("bedrock")
65
+
66
+
54
67
  def get_supported_llm_models(
55
68
  aws_region_name: str | None = None,
56
69
  aws_access_key_id: SecretStr | None = None,
@@ -68,9 +81,9 @@ def get_supported_llm_models(
68
81
  + list(litellm.model_cost.keys())
69
82
  + [f"openrouter/{model}" for model in VERIFIED_MODELS["openrouter"]]
70
83
  )
71
- litellm_model_list_without_bedrock = list(
72
- filter(lambda m: not m.startswith("bedrock"), litellm_model_list)
73
- )
84
+ litellm_model_list_without_bedrock = [
85
+ m for m in litellm_model_list if not _is_litellm_bedrock_model(m)
86
+ ]
74
87
  bedrock_model_list = []
75
88
  if aws_region_name and aws_access_key_id and aws_secret_access_key:
76
89
  bedrock_model_list = _list_bedrock_foundation_models(
@@ -76,6 +76,25 @@ class MarketplaceNotFoundError(PluginResolutionError):
76
76
  super().__init__(f"Marketplace '{marketplace_name}' is not registered")
77
77
 
78
78
 
79
+ def _load_marketplace(path: Path, name: str) -> Marketplace:
80
+ """Load the manifest, or read a source without one as a plain skills folder."""
81
+ try:
82
+ return Marketplace.load(path)
83
+ except FileNotFoundError as missing_manifest:
84
+ marketplace = Marketplace.from_skills_dir(path, name)
85
+ if not marketplace.skills:
86
+ raise FileNotFoundError(
87
+ f"{missing_manifest}, and no skill folders (<name>/SKILL.md) "
88
+ "were found there"
89
+ ) from missing_manifest
90
+ logger.info(
91
+ "Marketplace '%s' has no manifest; using %d skills from its folder",
92
+ name,
93
+ len(marketplace.skills),
94
+ )
95
+ return marketplace
96
+
97
+
79
98
  class MarketplaceRegistry:
80
99
  """Resolve plugin references against registered marketplaces."""
81
100
 
@@ -165,7 +184,7 @@ class MarketplaceRegistry:
165
184
  repo_path=registration.repo_path,
166
185
  )
167
186
  fetched = FetchedMarketplace(
168
- marketplace=Marketplace.load(path),
187
+ marketplace=_load_marketplace(path, registration.name),
169
188
  path=path,
170
189
  resolved_ref=resolved_ref,
171
190
  )
@@ -8,6 +8,7 @@ from typing import Any
8
8
 
9
9
  from pydantic import BaseModel, Field, field_validator, model_validator
10
10
 
11
+ from openhands.sdk.logger import get_logger
11
12
  from openhands.sdk.plugin.types import (
12
13
  HooksConfigDict,
13
14
  LspServersDict,
@@ -17,6 +18,8 @@ from openhands.sdk.plugin.types import (
17
18
  )
18
19
 
19
20
 
21
+ logger = get_logger(__name__)
22
+
20
23
  # Directories to check for marketplace manifest
21
24
  MARKETPLACE_MANIFEST_DIRS = [".plugin", ".claude-plugin"]
22
25
  MARKETPLACE_MANIFEST_FILE = "marketplace.json"
@@ -260,6 +263,58 @@ class Marketplace(BaseModel):
260
263
 
261
264
  return cls.model_validate({**data, "path": str(marketplace_dir)})
262
265
 
266
+ @classmethod
267
+ def from_skills_dir(cls, marketplace_path: str | Path, name: str) -> Marketplace:
268
+ """Build a marketplace from a plain folder of skills, without a manifest.
269
+
270
+ Each ``<name>/SKILL.md`` folder becomes a standalone skill: those in
271
+ ``skills/`` and ``.skills/``, or, when neither exists, those directly in
272
+ ``marketplace_path``, so a ``repo_path`` can point at the skills folder
273
+ itself. Loose markdown files are not read, so a repository's README and
274
+ docs never become skills. The result has no skills when none are found.
275
+
276
+ Args:
277
+ marketplace_path: Path to the repository or skills folder.
278
+ name: Marketplace name, since there is no manifest to supply one.
279
+
280
+ Returns:
281
+ Marketplace listing the discovered skills, with no plugins.
282
+ """
283
+ from openhands.sdk.skills import Skill
284
+ from openhands.sdk.skills.utils import find_skill_md_directories
285
+
286
+ marketplace_dir = Path(marketplace_path).resolve()
287
+ skills_dirs = [
288
+ path
289
+ for path in (marketplace_dir / "skills", marketplace_dir / ".skills")
290
+ if path.is_dir()
291
+ ] or [marketplace_dir]
292
+
293
+ entries: dict[str, MarketplaceEntry] = {}
294
+ for skills_dir in skills_dirs:
295
+ for skill_md in find_skill_md_directories(skills_dir, marketplace_dir):
296
+ try:
297
+ skill = Skill.load(skill_md, strict=False)
298
+ except Exception as e:
299
+ logger.warning(f"Failed to load skill from {skill_md}: {e}")
300
+ continue
301
+ source = skill_md.parent.relative_to(marketplace_dir).as_posix()
302
+ entries.setdefault(
303
+ skill.name,
304
+ MarketplaceEntry(
305
+ name=skill.name,
306
+ source=f"./{source}",
307
+ description=skill.description,
308
+ ),
309
+ )
310
+
311
+ return cls(
312
+ name=name,
313
+ owner=MarketplaceOwner(name=name),
314
+ skills=list(entries.values()),
315
+ path=str(marketplace_dir),
316
+ )
317
+
263
318
  def get_plugin(self, name: str) -> MarketplacePluginEntry | None:
264
319
  """Get a plugin entry by name.
265
320