openhands-sdk 1.57.0__tar.gz → 1.58.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 (329) hide show
  1. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/controller.py +6 -4
  3. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/local_conversation.py +14 -1
  4. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_diff.py +43 -5
  5. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/conversation_hooks.py +43 -1
  6. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/meta_profile_store.py +6 -1
  7. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/verified_models.py +3 -1
  8. openhands_sdk-1.58.0/openhands/sdk/mcp/oauth.py +203 -0
  9. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/utils.py +7 -2
  10. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile_store.py +16 -1
  11. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/command.py +11 -10
  12. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/models.py +14 -1
  13. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/base.py +2 -2
  14. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/repo.py +52 -11
  15. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  16. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/pyproject.toml +1 -1
  17. openhands_sdk-1.57.0/openhands/sdk/mcp/oauth.py +0 -85
  18. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/__init__.py +0 -0
  19. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/__init__.py +0 -0
  20. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_agent.py +0 -0
  21. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  22. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_file_credentials.py +0 -0
  23. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  24. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_models.py +0 -0
  25. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  26. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/agent.py +0 -0
  27. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/base.py +0 -0
  28. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  29. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  30. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  31. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/stream_context.py +0 -0
  32. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/utils.py +0 -0
  33. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/automation/__init__.py +0 -0
  34. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/banner.py +0 -0
  35. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/__init__.py +0 -0
  36. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/agent_context.py +0 -0
  37. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  38. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/base.py +0 -0
  39. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  40. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  41. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  42. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  43. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  44. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/utils.py +0 -0
  45. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/memory.py +0 -0
  46. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  47. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/presets.py +0 -0
  48. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  49. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/registry.py +0 -0
  50. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/section.py +0 -0
  51. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  52. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  53. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  54. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
  55. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  56. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  57. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/__init__.py +0 -0
  58. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  59. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  60. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/base.py +0 -0
  61. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  62. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  63. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  64. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  65. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/view.py +0 -0
  66. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/__init__.py +0 -0
  67. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/base.py +0 -0
  68. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/cancellation.py +0 -0
  69. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation.py +0 -0
  70. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  71. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/event_store.py +0 -0
  72. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  73. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/exceptions.py +0 -0
  74. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  75. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  76. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  77. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  78. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  79. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  80. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/remote_conversation.py +0 -0
  81. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/message_request.py +0 -0
  82. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  83. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/request.py +0 -0
  84. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  85. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/response_utils.py +0 -0
  86. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  87. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/state.py +0 -0
  88. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  89. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/title_utils.py +0 -0
  90. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/types.py +0 -0
  91. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  92. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  93. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  94. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/credential.py +0 -0
  95. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/__init__.py +0 -0
  96. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/base.py +0 -0
  97. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  98. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  99. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  100. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  101. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  102. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  103. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  104. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  105. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  106. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/result.py +0 -0
  107. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/__init__.py +0 -0
  108. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  109. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/base.py +0 -0
  110. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/condenser.py +0 -0
  111. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_error.py +0 -0
  112. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_state.py +0 -0
  113. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/error_classification.py +0 -0
  114. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/hook_execution.py +0 -0
  115. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  116. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  117. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  118. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  119. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  120. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  121. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  122. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/resume_transcript.py +0 -0
  123. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/security.py +0 -0
  124. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/streaming_delta.py +0 -0
  125. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/token.py +0 -0
  126. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/types.py +0 -0
  127. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/event/user_action.py +0 -0
  128. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/__init__.py +0 -0
  129. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/fetch.py +0 -0
  130. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  131. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/info.py +0 -0
  132. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  133. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  134. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  135. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  136. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/cached_repo.py +0 -0
  137. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/exceptions.py +0 -0
  138. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_changes.py +0 -0
  139. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_commits.py +0 -0
  140. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/models.py +0 -0
  141. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/git/utils.py +0 -0
  142. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/__init__.py +0 -0
  143. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/config.py +0 -0
  144. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/executor.py +0 -0
  145. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/manager.py +0 -0
  146. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/types.py +0 -0
  147. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/__init__.py +0 -0
  148. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/base.py +0 -0
  149. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/cache.py +0 -0
  150. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/local.py +0 -0
  151. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/io/memory.py +0 -0
  152. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/__init__.py +0 -0
  153. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/errors.py +0 -0
  154. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/finalize.py +0 -0
  155. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/preview.py +0 -0
  156. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/resolve.py +0 -0
  157. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/__init__.py +0 -0
  158. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  159. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_response_stream.py +0 -0
  160. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  161. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  162. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  163. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/openai.py +0 -0
  164. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/call_context.py +0 -0
  165. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  166. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  167. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  168. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  169. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  170. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  171. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm.py +0 -0
  172. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
  173. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_registry.py +0 -0
  174. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_response.py +0 -0
  175. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/message.py +0 -0
  176. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  177. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  178. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  179. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/__init__.py +0 -0
  180. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  181. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/common.py +0 -0
  182. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  183. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  184. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/__init__.py +0 -0
  185. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/base.py +0 -0
  186. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  187. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  188. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/streaming.py +0 -0
  189. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  190. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  191. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  192. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  193. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_features.py +0 -0
  194. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  195. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  196. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
  197. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  198. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  199. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  200. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  201. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  202. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  203. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  204. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  205. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/__init__.py +0 -0
  206. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/_rich_handler.py +0 -0
  207. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/logger.py +0 -0
  208. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/rolling.py +0 -0
  209. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/__init__.py +0 -0
  210. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registration.py +0 -0
  211. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registry.py +0 -0
  212. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/types.py +0 -0
  213. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/__init__.py +0 -0
  214. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/client.py +0 -0
  215. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/config.py +0 -0
  216. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/definition.py +0 -0
  217. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/exceptions.py +0 -0
  218. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/tool.py +0 -0
  219. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/__init__.py +0 -0
  220. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/laminar.py +0 -0
  221. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/utils.py +0 -0
  222. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/__init__.py +0 -0
  223. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/discovery.py +0 -0
  224. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/fetch.py +0 -0
  225. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  226. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
  227. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  228. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/base.py +0 -0
  229. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  230. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  231. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  232. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/installed.py +0 -0
  233. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/loader.py +0 -0
  234. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/paths.py +0 -0
  235. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/plugin.py +0 -0
  236. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/source.py +0 -0
  237. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/strict_fetch.py +0 -0
  238. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/types.py +0 -0
  239. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/__init__.py +0 -0
  240. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile.py +0 -0
  241. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/profile_refs.py +0 -0
  242. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/resolver.py +0 -0
  243. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/seed.py +0 -0
  244. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/py.typed +0 -0
  245. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/__init__.py +0 -0
  246. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/secrets.py +0 -0
  247. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/__init__.py +0 -0
  248. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/_shell_ast.py +0 -0
  249. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/analyzer.py +0 -0
  250. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  251. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  252. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  253. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  254. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
  255. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  256. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/ensemble.py +0 -0
  257. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  258. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  259. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  260. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  261. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/risk.py +0 -0
  262. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/shell_parser.py +0 -0
  263. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  264. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  265. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/__init__.py +0 -0
  266. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
  267. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_providers.py +0 -0
  268. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/api_models.py +0 -0
  269. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/metadata.py +0 -0
  270. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/model.py +0 -0
  271. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/__init__.py +0 -0
  272. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/exceptions.py +0 -0
  273. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/execute.py +0 -0
  274. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/fetch.py +0 -0
  275. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/installed.py +0 -0
  276. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/skill.py +0 -0
  277. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/trigger.py +0 -0
  278. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/types.py +0 -0
  279. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/utils.py +0 -0
  280. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/__init__.py +0 -0
  281. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/load.py +0 -0
  282. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/registry.py +0 -0
  283. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/schema.py +0 -0
  284. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/scope.py +0 -0
  285. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/__init__.py +0 -0
  286. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/test_llm.py +0 -0
  287. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/__init__.py +0 -0
  288. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  289. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  290. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  291. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  292. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  293. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/think.py +0 -0
  294. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  295. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/client_tool.py +0 -0
  296. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/defaults.py +0 -0
  297. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/registry.py +0 -0
  298. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/schema.py +0 -0
  299. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/spec.py +0 -0
  300. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/tool.py +0 -0
  301. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/__init__.py +0 -0
  302. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_executor.py +0 -0
  303. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_utils.py +0 -0
  304. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/cipher.py +0 -0
  305. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/datetime.py +0 -0
  306. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/deprecation.py +0 -0
  307. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/files.py +0 -0
  308. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/github.py +0 -0
  309. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/json.py +0 -0
  310. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/masking.py +0 -0
  311. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/paging.py +0 -0
  312. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/path.py +0 -0
  313. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  314. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/redact.py +0 -0
  315. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/truncate.py +0 -0
  316. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/visualize.py +0 -0
  317. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/__init__.py +0 -0
  318. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/base.py +0 -0
  319. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/local.py +0 -0
  320. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/models.py +0 -0
  321. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  322. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  323. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  324. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/workspace.py +0 -0
  325. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
  326. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  327. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/requires.txt +0 -0
  328. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  329. {openhands_sdk-1.57.0 → openhands_sdk-1.58.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.57.0
3
+ Version: 1.58.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
@@ -103,12 +103,14 @@ class GoalController:
103
103
  def on_run_finished(self, events: Sequence[Event]) -> GoalStep:
104
104
  """Judge the objective after a run and decide whether to continue.
105
105
 
106
- Increments the iteration count, audits ``events`` with the judge LLM,
107
- and returns a :class:`GoalContinue` (with a followup) or a terminal
108
- :class:`GoalDone` (with a :class:`GoalOutcome`).
106
+ Audits ``events`` with the judge LLM, counts the round once the judge
107
+ has returned a verdict, and returns a :class:`GoalContinue` (with a
108
+ followup) or a terminal :class:`GoalDone` (with a :class:`GoalOutcome`).
109
+ A judge call that raises leaves the count unchanged: a failed or
110
+ abandoned audit is not a completed round.
109
111
  """
110
- self.iteration += 1
111
112
  verdict = judge_goal(self.judge_llm, self.objective, events)
113
+ self.iteration += 1
112
114
  logger.info(
113
115
  "Goal audit %d/%d: score=%.2f complete=%s",
114
116
  self.iteration,
@@ -446,8 +446,14 @@ class LocalConversation(BaseConversation):
446
446
  # Single chokepoint: stamps parent_id (catching any event a hook
447
447
  # swapped in downstream of _tree_stamping) and advances HEAD.
448
448
  self._state.append_event(e)
449
+
450
+ def _track_last_user_message(e):
449
451
  # Track user MessageEvent IDs here so hook callbacks (which may
450
452
  # synthesize or alter user messages) are captured in one place.
453
+ # This runs after the caller-supplied callbacks: setting the field
454
+ # can emit a ConversationStateUpdateEvent through on_state_change
455
+ # (the agent server wires it to this chain), and that update must
456
+ # reach subscribers after the message that caused it.
451
457
  if isinstance(e, MessageEvent) and e.source == "user":
452
458
  # Track the latest real user message ID for hook-blocked checks.
453
459
  # Stop-hook feedback is emitted with source="environment".
@@ -461,7 +467,7 @@ class LocalConversation(BaseConversation):
461
467
  # list never run. The visualizer is prepended below and so still renders
462
468
  # ahead of persist — that is local terminal output, not an announcement
463
469
  # a client can act on.
464
- composed_list = [_default_callback] + callback_list
470
+ composed_list = [_default_callback] + callback_list + [_track_last_user_message]
465
471
  # Handle visualization configuration
466
472
  if isinstance(visualizer, ConversationVisualizerBase):
467
473
  # Use custom visualizer instance
@@ -1605,6 +1611,13 @@ class LocalConversation(BaseConversation):
1605
1611
  self._state,
1606
1612
  on_event=self._on_event,
1607
1613
  )
1614
+
1615
+ # Deliver any SessionStart additional_context now that
1616
+ # init_state() has emitted the SystemPromptEvent, so the system
1617
+ # prompt stays first in the LLM request. The hook processor
1618
+ # collected it earlier during _ensure_plugins_loaded().
1619
+ if self._hook_processor is not None:
1620
+ self._hook_processor.inject_pending_additional_context()
1608
1621
  except Exception:
1609
1622
  self._close_runtime_tools(runtime_mcp_tools)
1610
1623
  raise
@@ -50,6 +50,21 @@ def get_closest_git_repo(path: Path) -> Path | None:
50
50
  current_path = parent
51
51
 
52
52
 
53
+ def _base_file_size(repo: Path, ref: str, relative_path: Path) -> int | None:
54
+ """Size of the file (blob) ``relative_path`` at ``ref``, or ``None``
55
+ when ``ref`` has no file at that path."""
56
+ spec = f"{ref}:{relative_path.as_posix()}"
57
+ try:
58
+ object_type = run_git_command(
59
+ ["git", "cat-file", "-t", spec], repo, expected_failure=True
60
+ )
61
+ if object_type != "blob":
62
+ return None
63
+ return int(run_git_command(["git", "cat-file", "-s", spec], repo))
64
+ except (GitCommandError, ValueError):
65
+ return None
66
+
67
+
53
68
  def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitDiff:
54
69
  """Get git diff for a single file.
55
70
 
@@ -63,20 +78,23 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
63
78
  GitDiff object containing diff information
64
79
 
65
80
  Raises:
66
- GitPathError: If file is too large or doesn't exist
81
+ GitPathError: If file is too large, or exists neither on disk nor
82
+ at the base ref (a working-tree deletion diffs as the base text
83
+ against ``""``)
67
84
  GitRepositoryError: If not in a git repository
68
85
  GitCommandError: If git commands fail (including when ``ref`` is
69
86
  provided but does not resolve in the repository).
70
87
  """
71
88
  path = Path(os.getcwd(), relative_file_path).resolve()
89
+ missing_error = GitPathError(f"File does not exist: {path}")
72
90
 
73
- # Check if file exists
74
- if not path.exists():
75
- raise GitPathError(f"File does not exist: {path}")
91
+ # A path missing from disk may still be a working-tree deletion (or the
92
+ # old side of a rename) that only exists at the base ref.
93
+ deleted = not path.exists()
76
94
 
77
95
  # Check file size
78
96
  try:
79
- file_size = os.path.getsize(path)
97
+ file_size = 0 if deleted else os.path.getsize(path)
80
98
  if file_size > MAX_FILE_SIZE_FOR_GIT_DIFF:
81
99
  raise GitPathError(
82
100
  f"File too large for git diff: {file_size} bytes "
@@ -88,6 +106,8 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
88
106
  # Find git repository
89
107
  closest_git_repo = get_closest_git_repo(path)
90
108
  if not closest_git_repo:
109
+ if deleted:
110
+ raise missing_error
91
111
  raise GitRepositoryError(f"File is not in a git repository: {path}")
92
112
 
93
113
  # Validate the git repository
@@ -97,6 +117,8 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
97
117
  # so the per-file diff lines up with the change list it was opened from.
98
118
  current_rev = get_valid_ref(validated_repo, override=ref, purpose="display")
99
119
  if not current_rev:
120
+ if deleted:
121
+ raise missing_error
100
122
  logger.warning(f"No valid git reference found for {validated_repo}")
101
123
  return GitDiff(modified="", original="")
102
124
 
@@ -106,6 +128,18 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
106
128
  except ValueError as e:
107
129
  raise GitPathError(f"File is not within git repository: {path}") from e
108
130
 
131
+ if deleted:
132
+ base_size = _base_file_size(
133
+ validated_repo, current_rev, relative_path_from_repo
134
+ )
135
+ if base_size is None:
136
+ raise missing_error
137
+ if base_size > MAX_FILE_SIZE_FOR_GIT_DIFF:
138
+ raise GitPathError(
139
+ f"File too large for git diff: {base_size} bytes "
140
+ f"(max: {MAX_FILE_SIZE_FOR_GIT_DIFF} bytes)"
141
+ )
142
+
109
143
  # Get old content (from the ref)
110
144
  try:
111
145
  # Read the blob verbatim and normalize it like the modified side below,
@@ -121,6 +155,10 @@ def get_git_diff(relative_file_path: str | Path, ref: str | None = None) -> GitD
121
155
  logger.debug(f"No old content found for {path} at ref {current_rev}")
122
156
  original = ""
123
157
 
158
+ if deleted:
159
+ logger.info(f"Generated git diff for deleted file {path}")
160
+ return GitDiff(modified="", original=original)
161
+
124
162
  # Get new content (current file)
125
163
  try:
126
164
  with open(path, encoding="utf-8") as f:
@@ -15,7 +15,7 @@ from openhands.sdk.hooks.config import HookConfig
15
15
  from openhands.sdk.hooks.executor import HookResult
16
16
  from openhands.sdk.hooks.manager import HookManager
17
17
  from openhands.sdk.hooks.types import HookEventType
18
- from openhands.sdk.llm import TextContent
18
+ from openhands.sdk.llm import Message, TextContent
19
19
  from openhands.sdk.logger import get_logger
20
20
 
21
21
 
@@ -66,6 +66,9 @@ class HookEventProcessor:
66
66
  self.original_callback = original_callback
67
67
  self._conversation_state: ConversationState | None = None
68
68
  self.emit_hook_events = emit_hook_events
69
+ # SessionStart additional_context collected by run_session_start() and
70
+ # delivered later by inject_pending_additional_context(); see those methods.
71
+ self._pending_additional_context: list[str] = []
69
72
 
70
73
  def set_conversation_state(self, state: "ConversationState") -> None:
71
74
  """Set conversation state for blocking support."""
@@ -338,6 +341,45 @@ class HookEventProcessor:
338
341
  if result.error:
339
342
  logger.warning(f"SessionStart hook error: {result.error}")
340
343
 
344
+ # Collect additional_context for delivery to the agent, but do not emit
345
+ # it here. run_session_start() runs while plugins load, before the agent
346
+ # emits its SystemPromptEvent, so emitting now would place a user message
347
+ # ahead of the system prompt and break the system-first invariant (and the
348
+ # prompt cache, which only applies to a leading system message). The
349
+ # conversation flushes it with inject_pending_additional_context() once
350
+ # init_state() has emitted the system prompt.
351
+ self._pending_additional_context.extend(
352
+ r.additional_context for r in results if r.additional_context
353
+ )
354
+
355
+ def inject_pending_additional_context(self) -> None:
356
+ """Deliver collected SessionStart additional_context to the agent.
357
+
358
+ Emits the context as a single ``MessageEvent``. Call this after the
359
+ agent's ``SystemPromptEvent`` is in the log so the system prompt stays
360
+ first in the request, mirroring the ``UserPromptSubmit`` path (which
361
+ extends the existing user turn rather than prepending a new one). The
362
+ pending context is cleared on delivery, so re-running SessionStart hooks
363
+ (for example when runtime plugin hooks merge) does not deliver it twice.
364
+ """
365
+ if not self._pending_additional_context or not self.original_callback:
366
+ return
367
+
368
+ additional_context = "\n".join(self._pending_additional_context)
369
+ self._pending_additional_context = []
370
+ logger.debug(
371
+ f"SessionStart hook injecting context: {additional_context[:100]}..."
372
+ )
373
+ self.original_callback(
374
+ MessageEvent(
375
+ source="environment",
376
+ llm_message=Message(
377
+ role="user",
378
+ content=[TextContent(text=additional_context)],
379
+ ),
380
+ )
381
+ )
382
+
341
383
  def run_session_end(self) -> None:
342
384
  """Run SessionEnd hooks. Call before conversation is closed."""
343
385
  hooks = self.hook_manager.config.get_hooks_for_event(HookEventType.SESSION_END)
@@ -88,7 +88,12 @@ class MetaProfile(BaseModel):
88
88
 
89
89
  @model_validator(mode="after")
90
90
  def validate_routing_mode(self) -> MetaProfile:
91
- """Require either structured classes or a direct prompt, not both."""
91
+ """Validate direct-prompt mode: a template needs ``{{ instance_text }}``
92
+ and excludes classes.
93
+
94
+ A meta-profile with neither classes nor a template is accepted here so
95
+ persisted ones still load; ``POST /api/meta-profiles/{name}`` refuses it.
96
+ """
92
97
  if self.prompt_template is None:
93
98
  return self
94
99
  if re.search(r"{{\s*instance_text\s*}}", self.prompt_template) is None:
@@ -70,8 +70,9 @@ VERIFIED_MOONSHOT_MODELS = [
70
70
  "kimi-for-coding",
71
71
  ]
72
72
 
73
- # M3 and M2.7.
73
+ # M3 (incl. flash) and M2.7.
74
74
  VERIFIED_MINIMAX_MODELS = [
75
+ "minimax-m3.1-flash",
75
76
  "minimax-m3",
76
77
  "minimax-m2.7",
77
78
  ]
@@ -172,6 +173,7 @@ VERIFIED_OPENHANDS_MODELS = [
172
173
  "gpt-5.6",
173
174
  "gpt-5.3-codex",
174
175
  "gpt-5.2-codex",
176
+ "minimax-m3.1-flash",
175
177
  "minimax-m3",
176
178
  "minimax-m2.7",
177
179
  "gemini-3.1-pro",
@@ -0,0 +1,203 @@
1
+ """FastMCP OAuth client that discovers the token endpoint before refreshing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import AsyncGenerator
6
+ from contextlib import aclosing
7
+ from typing import Any, Literal
8
+ from urllib.parse import parse_qsl
9
+
10
+ import httpx
11
+ from fastmcp.client.auth import OAuth
12
+ from mcp.client.auth.utils import (
13
+ build_oauth_authorization_server_metadata_discovery_urls,
14
+ build_protected_resource_metadata_discovery_urls,
15
+ create_client_registration_request,
16
+ create_oauth_metadata_request,
17
+ handle_auth_metadata_response,
18
+ handle_protected_resource_response,
19
+ )
20
+
21
+ from openhands.sdk.logger import get_logger
22
+ from openhands.sdk.mcp.exceptions import MCPError
23
+
24
+
25
+ logger = get_logger(__name__)
26
+
27
+ _DISCOVERY_TIMEOUT = httpx.Timeout(10.0)
28
+
29
+ _TokenEndpointAuthMethod = Literal["none", "client_secret_post", "client_secret_basic"]
30
+
31
+ # Most preferred first: a public client is never issued a secret to keep.
32
+ _TOKEN_ENDPOINT_AUTH_METHODS: tuple[_TokenEndpointAuthMethod, ...] = (
33
+ "none",
34
+ "client_secret_post",
35
+ "client_secret_basic",
36
+ )
37
+
38
+
39
+ def _preferred_token_endpoint_auth_method(
40
+ supported: list[str] | None,
41
+ ) -> _TokenEndpointAuthMethod | None:
42
+ for method in _TOKEN_ENDPOINT_AUTH_METHODS:
43
+ if method in (supported or ()):
44
+ return method
45
+ return None
46
+
47
+
48
+ def _without_client_id_when_basic(request: httpx.Request) -> httpx.Request:
49
+ """Drop ``client_id`` from the form body of a Basic-authenticated request.
50
+
51
+ RFC 6749 section 2.3.1 forbids combining the ``Authorization`` header with
52
+ body credentials, and strict servers reject the mix as using multiple
53
+ authentication methods.
54
+ """
55
+ if not request.headers.get("Authorization", "").startswith("Basic "):
56
+ return request
57
+ data = dict(parse_qsl(request.content.decode(), keep_blank_values=True))
58
+ data.pop("client_id", None)
59
+ headers = request.headers.copy()
60
+ headers.pop("Content-Length", None)
61
+ return httpx.Request(request.method, request.url, data=data, headers=headers)
62
+
63
+
64
+ class MCPOAuth(OAuth):
65
+ """FastMCP ``OAuth`` whose token refresh targets the discovered endpoint.
66
+
67
+ The upstream ``OAuthClientProvider`` refreshes an expired access token
68
+ before sending a request, but discovers the authorization server's
69
+ metadata only inside its 401 branch and never persists it. Every
70
+ conversation builds a fresh client, so ``_refresh_token`` falls back to
71
+ ``<MCP server origin>/token``, which is wrong for providers such as GitLab
72
+ (``/oauth/token``) and Atlassian (``/v1/token``). The 404 that follows
73
+ drops the tokens and starts the interactive browser flow, which cannot
74
+ complete inside a sandbox.
75
+
76
+ ``_refresh_token`` runs under upstream's context lock after
77
+ ``_initialize()`` restored the stored tokens and expiry, so discovery only
78
+ happens when a refresh is actually about to be attempted.
79
+
80
+ Dynamic client registration asks for a token endpoint auth method the
81
+ authorization server advertises, preferring a public client. Upstream
82
+ sends none, so servers fall back to the RFC 7591 default
83
+ ``client_secret_basic`` and issue a secret. Token requests that do use
84
+ Basic auth leave ``client_id`` out of the body, which upstream always
85
+ includes and which strict servers reject.
86
+ """
87
+
88
+ async def async_auth_flow(
89
+ self, request: httpx.Request
90
+ ) -> AsyncGenerator[httpx.Request, httpx.Response]:
91
+ async with aclosing(super().async_auth_flow(request)) as flow:
92
+ outgoing = await anext(flow)
93
+ while True:
94
+ response = yield self._with_supported_auth_method(outgoing)
95
+ try:
96
+ outgoing = await flow.asend(response)
97
+ except StopAsyncIteration:
98
+ return
99
+
100
+ def _with_supported_auth_method(self, request: httpx.Request) -> httpx.Request:
101
+ """Rebuild the registration request around a supported auth method.
102
+
103
+ Any other request, an explicitly configured method and a server that
104
+ advertises no known method all pass through unchanged.
105
+ """
106
+ context = self.context
107
+ if (
108
+ context.client_info is not None
109
+ or context.oauth_metadata is None
110
+ or context.client_metadata.token_endpoint_auth_method is not None
111
+ ):
112
+ return request
113
+ method = _preferred_token_endpoint_auth_method(
114
+ context.oauth_metadata.token_endpoint_auth_methods_supported
115
+ )
116
+ if method is None or request.url != self._registration_request().url:
117
+ return request
118
+ context.client_metadata.token_endpoint_auth_method = method
119
+ return self._registration_request()
120
+
121
+ def _registration_request(self) -> httpx.Request:
122
+ return create_client_registration_request(
123
+ self.context.oauth_metadata,
124
+ self.context.client_metadata,
125
+ self.context.get_authorization_base_url(self.context.server_url),
126
+ )
127
+
128
+ async def _exchange_token_authorization_code(
129
+ self,
130
+ auth_code: str,
131
+ code_verifier: str,
132
+ *,
133
+ token_data: dict[str, Any] | None = None,
134
+ ) -> httpx.Request:
135
+ request = await super()._exchange_token_authorization_code(
136
+ auth_code, code_verifier, token_data=token_data
137
+ )
138
+ return _without_client_id_when_basic(request)
139
+
140
+ async def _refresh_token(self) -> httpx.Request:
141
+ if self.context.oauth_metadata is None:
142
+ await self._discover_oauth_metadata()
143
+ return _without_client_id_when_basic(await super()._refresh_token())
144
+
145
+ async def _discover_oauth_metadata(self) -> None:
146
+ """Populate the resource and authorization-server metadata.
147
+
148
+ Mirrors the discovery upstream performs on a 401 (protected resource
149
+ metadata first, then authorization server metadata, with the same
150
+ fallback URL lists). A network failure only logs: the refresh then
151
+ goes to upstream's default endpoint, exactly as before.
152
+ """
153
+ server_url = self.context.server_url
154
+ try:
155
+ async with self.httpx_client_factory(timeout=_DISCOVERY_TIMEOUT) as client:
156
+ for url in build_protected_resource_metadata_discovery_urls(
157
+ None, server_url
158
+ ):
159
+ response = await client.send(create_oauth_metadata_request(url))
160
+ prm = await handle_protected_resource_response(response)
161
+ if prm is None:
162
+ continue
163
+ await self._validate_resource_match(prm)
164
+ self.context.protected_resource_metadata = prm
165
+ # ``authorization_servers`` has a minimum length of one.
166
+ self.context.auth_server_url = str(prm.authorization_servers[0])
167
+ break
168
+
169
+ for url in build_oauth_authorization_server_metadata_discovery_urls(
170
+ self.context.auth_server_url, server_url
171
+ ):
172
+ response = await client.send(create_oauth_metadata_request(url))
173
+ ok, metadata = await handle_auth_metadata_response(response)
174
+ if not ok:
175
+ break
176
+ if metadata is not None:
177
+ self.context.oauth_metadata = metadata
178
+ break
179
+ except httpx.HTTPError as exc:
180
+ logger.warning(
181
+ "OAuth metadata discovery failed for %s; refreshing against the "
182
+ "default token endpoint: %s",
183
+ server_url,
184
+ exc,
185
+ )
186
+
187
+
188
+ class NonInteractiveMCPOAuth(MCPOAuth):
189
+ """``MCPOAuth`` that only uses the stored tokens and refreshes them.
190
+
191
+ When no stored token is usable, FastMCP starts a new authorization: it
192
+ preflights the authorization URL with a loopback redirect URI, and if the
193
+ server rejects that URI it clears the token storage and registers a new
194
+ client before opening a browser. Nobody can complete that flow for an
195
+ unattended client, so this one fails the connection instead and leaves
196
+ the stored state for the user to replace by authorizing again.
197
+ """
198
+
199
+ async def redirect_handler(self, authorization_url: str) -> None: # noqa: ARG002
200
+ raise MCPError(
201
+ f"MCP server {self.mcp_url} must be authorized again: its stored "
202
+ "OAuth tokens are missing or could not be refreshed."
203
+ )
@@ -95,6 +95,7 @@ def _oauth_auth_from_authentication_config(
95
95
  authentication: MCPOAuthAuthentication | None,
96
96
  *,
97
97
  mcp_oauth_token_storage: AsyncKeyValue | None = None,
98
+ mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
98
99
  ) -> OAuth | None:
99
100
  """Build FastMCP OAuth auth from explicit SDK MCP auth metadata."""
100
101
  if authentication is None:
@@ -105,7 +106,7 @@ def _oauth_auth_from_authentication_config(
105
106
  if client_auth_method is not None:
106
107
  additional_client_metadata["token_endpoint_auth_method"] = client_auth_method
107
108
 
108
- return MCPOAuth(
109
+ return mcp_oauth_class(
109
110
  scopes=authentication.scopes,
110
111
  client_name=authentication.client_name or "FastMCP Client",
111
112
  token_storage=mcp_oauth_token_storage,
@@ -169,6 +170,7 @@ def _prepare_mcp_config(
169
170
  *,
170
171
  mcp_oauth_token_storage: AsyncKeyValue | None = None,
171
172
  mcp_oauth_factory: MCPOAuthFactory | None = None,
173
+ mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
172
174
  ) -> FastMCPConfig:
173
175
  """Validate MCP config and apply explicit OpenHands runtime auth metadata."""
174
176
  prepared = FastMCPConfig.model_validate(to_fastmcp_mcp_config(mcp_config))
@@ -198,12 +200,13 @@ def _prepare_mcp_config(
198
200
  else _oauth_auth_from_authentication_config(
199
201
  auth.authentication,
200
202
  mcp_oauth_token_storage=mcp_oauth_token_storage,
203
+ mcp_oauth_class=mcp_oauth_class,
201
204
  )
202
205
  )
203
206
  if oauth_auth is not None:
204
207
  server.auth = oauth_auth
205
208
  elif mcp_oauth_token_storage is not None:
206
- server.auth = MCPOAuth(token_storage=mcp_oauth_token_storage)
209
+ server.auth = mcp_oauth_class(token_storage=mcp_oauth_token_storage)
207
210
 
208
211
  return prepared
209
212
 
@@ -377,6 +380,7 @@ def create_mcp_tools(
377
380
  on_tools_reconciled: ToolsReconciledCallback | None = None,
378
381
  mcp_oauth_token_storage: AsyncKeyValue | None = None,
379
382
  mcp_oauth_factory: MCPOAuthFactory | None = None,
383
+ mcp_oauth_class: type[MCPOAuth] = MCPOAuth,
380
384
  ) -> MCPClient:
381
385
  """Create MCP tools from OpenHands-native MCP server settings.
382
386
 
@@ -408,6 +412,7 @@ def create_mcp_tools(
408
412
  mcp_config,
409
413
  mcp_oauth_token_storage=mcp_oauth_token_storage,
410
414
  mcp_oauth_factory=mcp_oauth_factory,
415
+ mcp_oauth_class=mcp_oauth_class,
411
416
  )
412
417
  handler = _ToolListChangedHandler(
413
418
  client=None, # type: ignore[arg-type]
@@ -40,6 +40,15 @@ PROFILE_NAME_REGEX: Final[re.Pattern[str]] = re.compile(PROFILE_NAME_PATTERN)
40
40
  logger = get_logger(__name__)
41
41
 
42
42
 
43
+ def canonical_profile_name(name: str) -> str:
44
+ """Return the store key for ``name``: a trailing ``.json`` is dropped.
45
+
46
+ The file store keys a profile by its file stem, so ``x.json`` and ``x``
47
+ name the same profile.
48
+ """
49
+ return name.removesuffix(".json")
50
+
51
+
43
52
  class ProfileLimitExceeded(Exception):
44
53
  """Raised when saving would exceed the configured profile limit."""
45
54
 
@@ -107,7 +116,7 @@ class AgentProfileStore:
107
116
  Raises:
108
117
  ValueError: If ``name`` does not match ``PROFILE_NAME_PATTERN``.
109
118
  """
110
- clean_name = name.removesuffix(".json")
119
+ clean_name = canonical_profile_name(name)
111
120
  if not PROFILE_NAME_REGEX.match(clean_name):
112
121
  raise ValueError(
113
122
  f"Invalid profile name: {name!r}. "
@@ -153,6 +162,9 @@ class AgentProfileStore:
153
162
  """
154
163
  profile_path = self._get_profile_path(profile.name)
155
164
  payload = profile.model_dump(mode="json")
165
+ # Keep the stored name equal to the file stem (as ``rename`` does), so
166
+ # ``x.json`` and ``x`` are one profile with one name.
167
+ payload["name"] = profile_path.stem
156
168
 
157
169
  with self._acquire_lock():
158
170
  if max_profiles is not None and not profile_path.exists():
@@ -420,6 +432,9 @@ def save_profile_preserving_identity(
420
432
  Raises:
421
433
  ProfileLimitExceeded: If ``max_profiles`` would be exceeded.
422
434
  """
435
+ name = canonical_profile_name(profile.name)
436
+ if name != profile.name:
437
+ profile = profile.model_copy(update={"name": name})
423
438
  with store.lock():
424
439
  existing_id, existing_rev = _existing_identity(store, profile.name)
425
440
  if existing_id is not None:
@@ -13,16 +13,17 @@ from openhands.sdk.utils.redact import redact_text_secrets
13
13
  logger = get_logger(__name__)
14
14
 
15
15
 
16
- # Env vars that should not be exposed to subprocesses (e.g., bash commands
17
- # executed by the agent). These credentials allow access to user secrets via
18
- # the SaaS API and/or decrypting persisted secrets, and must remain isolated to
19
- # the SDK's Python process.
20
- #
21
- # - ``SESSION_API_KEY``: legacy (V0) session key name.
22
- # - ``OH_SECRET_KEY``: cipher key that decrypts persisted conversation/provider
23
- # secrets; leaking it is at least as damaging as leaking the session key.
24
- # See ``openhands.agent_server.config`` for where these are read from the env.
25
- _SENSITIVE_ENV_VARS = frozenset({"SESSION_API_KEY", "OH_SECRET_KEY"})
16
+ # Credentials needed by the SDK/server, not by agent-run subprocesses.
17
+ _SENSITIVE_ENV_VARS = frozenset(
18
+ {
19
+ "SESSION_API_KEY",
20
+ "OH_SECRET_KEY",
21
+ "LMNR_PROJECT_API_KEY",
22
+ "OTEL_HEADERS",
23
+ "OTEL_EXPORTER_OTLP_HEADERS",
24
+ "OTEL_EXPORTER_OTLP_TRACES_HEADERS",
25
+ }
26
+ )
26
27
 
27
28
  # Session keys are also delivered as an indexed list ``OH_SESSION_API_KEYS_0``,
28
29
  # ``OH_SESSION_API_KEYS_1``, ... (V1). Strip every slot by prefix so a rename or
@@ -43,6 +43,12 @@ def _is_abstract(type_: type) -> bool:
43
43
  return False
44
44
 
45
45
 
46
+ def _is_object_input(data: Any, cls: type) -> bool:
47
+ """Whether ``data`` is a dict or an instance of ``cls`` (a bool, so that
48
+ pyright does not narrow ``data`` at the call site)."""
49
+ return isinstance(data, (dict, cls))
50
+
51
+
46
52
  def get_handler_class_name(handler: SerializerFunctionWrapHandler) -> str:
47
53
  """Extract the class name from a Pydantic serializer handler's repr string.
48
54
 
@@ -209,8 +215,16 @@ class DiscriminatedUnionMixin(OpenHandsModel):
209
215
  def _validate_subtype(
210
216
  cls, data: Any, handler: ModelWrapValidatorHandler[Self], info: ValidationInfo
211
217
  ) -> Self:
218
+ if not _is_object_input(data, cls):
219
+ # Let pydantic report a non-object input (a string, number, null,
220
+ # list...) as a ValidationError instead of failing on .pop().
221
+ return handler(data)
212
222
  if isinstance(data, cls):
213
223
  return data
224
+ if isinstance(data, dict):
225
+ # Never mutate the caller's dict: callers such as RemoteState
226
+ # validate the same cached payload more than once.
227
+ data = dict(data)
214
228
  if not _is_abstract(cls):
215
229
  has_kind_alias_field = any(
216
230
  field_name != "kind" and field_info.alias == "kind"
@@ -229,7 +243,6 @@ class DiscriminatedUnionMixin(OpenHandsModel):
229
243
  None,
230
244
  )
231
245
  if internal_kind_field in data:
232
- data = data.copy()
233
246
  data.pop("kind", None)
234
247
  return handler(data)
235
248
  kind = data.pop("kind", None)
@@ -634,8 +634,8 @@ class RemoteWorkspace(RemoteWorkspaceMixin, BaseWorkspace):
634
634
 
635
635
  Clones specified repositories to meaningful directory names (e.g.,
636
636
  'openhands-cli' instead of 'repo_0'). Automatically fetches GitHub,
637
- GitLab, and Bitbucket tokens from the agent server's secrets for
638
- authentication.
637
+ GitLab, Bitbucket and Azure DevOps tokens from the agent server's
638
+ secrets for authentication.
639
639
 
640
640
  Args:
641
641
  repos: List of repositories to clone. Can be: