openhands-sdk 1.59.0__tar.gz → 1.60.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 (331) hide show
  1. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/PKG-INFO +4 -4
  2. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/__init__.py +4 -0
  3. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_agent.py +102 -14
  4. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_file_credentials.py +57 -3
  5. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/agent.py +15 -0
  6. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/base.py +11 -9
  7. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/stream_context.py +35 -2
  8. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/utils.py +36 -19
  9. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/static.py +4 -4
  10. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/base.py +11 -4
  11. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/event_store.py +1 -5
  12. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/exceptions.py +25 -0
  13. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/local_conversation.py +147 -47
  14. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/remote_conversation.py +32 -15
  15. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/request.py +1 -2
  16. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/response_utils.py +11 -1
  17. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/secret_registry.py +3 -1
  18. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/stuck_detector.py +163 -18
  19. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/title_utils.py +5 -11
  20. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/taxonomy.py +4 -1
  21. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/observation.py +14 -1
  22. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/fetch.py +3 -1
  23. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/manager.py +14 -4
  24. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/git_changes.py +41 -92
  25. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/git_commits.py +3 -2
  26. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/utils.py +11 -1
  27. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/io/memory.py +11 -3
  28. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/__init__.py +29 -0
  29. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/openai.py +4 -4
  30. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/call_context.py +8 -0
  31. openhands_sdk-1.60.0/openhands/sdk/llm/decision.py +580 -0
  32. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/mapping.py +7 -0
  33. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm.py +51 -10
  34. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_profile_store.py +1 -1
  35. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/message.py +11 -12
  36. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/fn_call_converter.py +20 -5
  37. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/responses_options.py +1 -0
  38. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_features.py +79 -6
  39. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_info.py +32 -10
  40. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/openhands_provider.py +16 -0
  41. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/retry_mixin.py +15 -0
  42. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/verified_models.py +16 -4
  43. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/tool.py +27 -1
  44. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/__init__.py +0 -6
  45. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/agent_profile.py +1 -1
  46. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/resolver.py +5 -216
  47. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/seed.py +1 -1
  48. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/analyzer.py +7 -1
  49. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/model.py +47 -7
  50. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/utils.py +3 -1
  51. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/subagent/registry.py +24 -5
  52. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/subagent/schema.py +4 -0
  53. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/__init__.py +9 -4
  54. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/schema.py +8 -1
  55. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/redact.py +3 -1
  56. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/base.py +2 -2
  57. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/repo.py +99 -10
  58. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/PKG-INFO +4 -4
  59. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/SOURCES.txt +2 -0
  60. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/requires.txt +3 -3
  61. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/pyproject.toml +4 -4
  62. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/__init__.py +0 -0
  63. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  64. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  65. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_models.py +0 -0
  66. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_skills.py +0 -0
  67. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  68. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  69. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  70. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  71. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/automation/__init__.py +0 -0
  72. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/banner.py +0 -0
  73. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/__init__.py +0 -0
  74. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/agent_context.py +0 -0
  75. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  76. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/base.py +0 -0
  77. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  78. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  79. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  80. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  81. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  82. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/utils.py +0 -0
  83. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/memory.py +0 -0
  84. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  85. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/presets.py +0 -0
  86. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  87. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/registry.py +0 -0
  88. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/section.py +0 -0
  89. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  90. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  91. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  92. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  93. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  94. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/__init__.py +0 -0
  95. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  96. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  97. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/base.py +0 -0
  98. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  99. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  100. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  101. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  102. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/context/view/view.py +0 -0
  103. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/__init__.py +0 -0
  104. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/cancellation.py +0 -0
  105. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/conversation.py +0 -0
  106. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  107. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  108. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  109. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  110. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/controller.py +0 -0
  111. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  112. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  113. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  114. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  115. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/message_request.py +0 -0
  116. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  117. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  118. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/state.py +0 -0
  119. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/types.py +0 -0
  120. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  121. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  122. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  123. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/credential.py +0 -0
  124. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/__init__.py +0 -0
  125. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/base.py +0 -0
  126. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  127. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  128. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  129. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  130. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  131. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  132. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  133. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  134. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/critic/result.py +0 -0
  135. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/__init__.py +0 -0
  136. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  137. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/base.py +0 -0
  138. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/condenser.py +0 -0
  139. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/conversation_error.py +0 -0
  140. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/conversation_state.py +0 -0
  141. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/error_classification.py +0 -0
  142. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/hook_execution.py +0 -0
  143. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  144. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  145. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  146. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  147. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  148. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  149. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/resume_transcript.py +0 -0
  150. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/security.py +0 -0
  151. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/streaming_delta.py +0 -0
  152. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/token.py +0 -0
  153. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/types.py +0 -0
  154. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/event/user_action.py +0 -0
  155. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/__init__.py +0 -0
  156. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  157. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/info.py +0 -0
  158. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  159. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  160. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  161. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/cached_repo.py +0 -0
  162. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/exceptions.py +0 -0
  163. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/git_diff.py +0 -0
  164. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/git/models.py +0 -0
  165. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/__init__.py +0 -0
  166. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/config.py +0 -0
  167. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  168. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/executor.py +0 -0
  169. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/manager.py +0 -0
  170. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/hooks/types.py +0 -0
  171. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/io/__init__.py +0 -0
  172. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/io/base.py +0 -0
  173. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/io/cache.py +0 -0
  174. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/io/local.py +0 -0
  175. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/launch/__init__.py +0 -0
  176. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/launch/errors.py +0 -0
  177. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/launch/finalize.py +0 -0
  178. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/launch/preview.py +0 -0
  179. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/launch/resolve.py +0 -0
  180. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  181. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/_response_stream.py +0 -0
  182. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  183. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  184. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  185. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  186. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  187. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  188. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  189. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  190. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_registry.py +0 -0
  191. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_response.py +0 -0
  192. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/meta_profile_store.py +0 -0
  193. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  194. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  195. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/__init__.py +0 -0
  196. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  197. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/common.py +0 -0
  198. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  199. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/__init__.py +0 -0
  200. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/base.py +0 -0
  201. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  202. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  203. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/streaming.py +0 -0
  204. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  205. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  206. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  207. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  208. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  209. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  210. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  211. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  212. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/retry_after.py +0 -0
  213. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  214. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  215. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  216. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  217. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/logger/__init__.py +0 -0
  218. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/logger/_rich_handler.py +0 -0
  219. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/logger/logger.py +0 -0
  220. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/logger/rolling.py +0 -0
  221. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/__init__.py +0 -0
  222. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/registration.py +0 -0
  223. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/registry.py +0 -0
  224. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/types.py +0 -0
  225. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/__init__.py +0 -0
  226. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/client.py +0 -0
  227. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/config.py +0 -0
  228. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/definition.py +0 -0
  229. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/exceptions.py +0 -0
  230. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/oauth.py +0 -0
  231. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/mcp/utils.py +0 -0
  232. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/observability/__init__.py +0 -0
  233. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/observability/laminar.py +0 -0
  234. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/observability/utils.py +0 -0
  235. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/__init__.py +0 -0
  236. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/discovery.py +0 -0
  237. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/fetch.py +0 -0
  238. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  239. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
  240. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  241. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/base.py +0 -0
  242. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  243. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  244. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  245. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/installed.py +0 -0
  246. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/loader.py +0 -0
  247. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/paths.py +0 -0
  248. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/plugin.py +0 -0
  249. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/source.py +0 -0
  250. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/strict_fetch.py +0 -0
  251. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/plugin/types.py +0 -0
  252. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/agent_profile_store.py +0 -0
  253. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/profiles/profile_refs.py +0 -0
  254. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/py.typed +0 -0
  255. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/secret/__init__.py +0 -0
  256. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/secret/secrets.py +0 -0
  257. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/__init__.py +0 -0
  258. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/_shell_ast.py +0 -0
  259. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/analyzer.py +0 -0
  260. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  261. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  262. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  263. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  264. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
  265. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  266. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/ensemble.py +0 -0
  267. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  268. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  269. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  270. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/risk.py +0 -0
  271. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/shell_parser.py +0 -0
  272. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  273. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  274. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/__init__.py +0 -0
  275. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
  276. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/acp_providers.py +0 -0
  277. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/api_models.py +0 -0
  278. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/settings/metadata.py +0 -0
  279. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/__init__.py +0 -0
  280. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/exceptions.py +0 -0
  281. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/execute.py +0 -0
  282. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/fetch.py +0 -0
  283. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/installed.py +0 -0
  284. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/skill.py +0 -0
  285. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/trigger.py +0 -0
  286. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/skills/types.py +0 -0
  287. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/subagent/__init__.py +0 -0
  288. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/subagent/load.py +0 -0
  289. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/subagent/scope.py +0 -0
  290. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/testing/__init__.py +0 -0
  291. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/testing/test_llm.py +0 -0
  292. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/__init__.py +0 -0
  293. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  294. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  295. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  296. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  297. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/think.py +0 -0
  298. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  299. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/client_tool.py +0 -0
  300. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/defaults.py +0 -0
  301. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/registry.py +0 -0
  302. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/spec.py +0 -0
  303. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/tool/tool.py +0 -0
  304. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/__init__.py +0 -0
  305. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/async_executor.py +0 -0
  306. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/async_utils.py +0 -0
  307. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/cipher.py +0 -0
  308. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/command.py +0 -0
  309. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/datetime.py +0 -0
  310. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/deprecation.py +0 -0
  311. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/files.py +0 -0
  312. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/github.py +0 -0
  313. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/json.py +0 -0
  314. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/masking.py +0 -0
  315. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/models.py +0 -0
  316. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/paging.py +0 -0
  317. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/path.py +0 -0
  318. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  319. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/truncate.py +0 -0
  320. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/utils/visualize.py +0 -0
  321. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/__init__.py +0 -0
  322. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/base.py +0 -0
  323. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/local.py +0 -0
  324. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/models.py +0 -0
  325. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  326. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  327. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  328. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands/sdk/workspace/workspace.py +0 -0
  329. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  330. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  331. {openhands_sdk-1.59.0 → openhands_sdk-1.60.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.59.0
3
+ Version: 1.60.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
@@ -14,9 +14,9 @@ Requires-Dist: fakeredis[lua]>=2.32.1
14
14
  Requires-Dist: fastmcp<4,>=3.2.0
15
15
  Requires-Dist: filelock>=3.20.1
16
16
  Requires-Dist: httpx[socks]>=0.28.1
17
- Requires-Dist: jsonschema>=4.23.0
17
+ Requires-Dist: jsonschema>=4.26.0
18
18
  Requires-Dist: joserfc>=1.7.5
19
- Requires-Dist: litellm<1.106,>=1.93.0
19
+ Requires-Dist: litellm<1.106,>=1.104.3
20
20
  Requires-Dist: pillow>=12.3.0
21
21
  Requires-Dist: pydantic>=2.13.5
22
22
  Requires-Dist: python-frontmatter>=1.3.0
@@ -31,4 +31,4 @@ Requires-Dist: boto3>=1.35.0; extra == "boto3"
31
31
  Provides-Extra: toolshield
32
32
  Requires-Dist: toolshield<0.2,>=0.1.3; extra == "toolshield"
33
33
  Provides-Extra: vertex
34
- Requires-Dist: google-cloud-aiplatform>=2.1.3; extra == "vertex"
34
+ Requires-Dist: google-cloud-aiplatform>=2.5.0; extra == "vertex"
@@ -26,6 +26,8 @@ from openhands.sdk.io import FileStore, LocalFileStore
26
26
  from openhands.sdk.llm import (
27
27
  LLM,
28
28
  LLM_PROFILE_SCHEMA_VERSION,
29
+ DecisionModel,
30
+ DecisionResult,
29
31
  FallbackStrategy,
30
32
  ImageContent,
31
33
  LLMProfileStore,
@@ -119,6 +121,8 @@ _print_banner(__version__)
119
121
 
120
122
  __all__ = [
121
123
  "LLM",
124
+ "DecisionModel",
125
+ "DecisionResult",
122
126
  "LLM_PROFILE_SCHEMA_VERSION",
123
127
  "LLMRegistry",
124
128
  "LLMProfileStore",
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import asyncio
20
20
  import atexit
21
21
  import contextlib
22
+ import functools
22
23
  import inspect
23
24
  import json
24
25
  import os
@@ -1342,6 +1343,14 @@ class _OpenHandsACPBridge:
1342
1343
  self.accumulated_text: list[str] = []
1343
1344
  self.accumulated_thoughts: list[str] = []
1344
1345
  self.accumulated_tool_calls: list[dict[str, Any]] = []
1346
+ # Text and thoughts said before a tool call are emitted as their own
1347
+ # message ahead of that call (``on_text_segment``), so the transcript
1348
+ # keeps the agent's narration where it was said. These mark where the
1349
+ # segment not yet emitted starts; the rest of the turn ends it.
1350
+ self._segment_text_start = 0
1351
+ self._segment_thought_start = 0
1352
+ self.text_segments_emitted = 0
1353
+ self.on_text_segment: Callable[[str, str], None] | None = None
1345
1354
  # Emits the LLM/TOOL spans an ACP turn would otherwise never produce.
1346
1355
  self.trace = ACPTurnTrace(acp_server=None, model_id=None)
1347
1356
  self.on_token: Any = None # ConversationTokenCallbackType | None
@@ -1387,6 +1396,10 @@ class _OpenHandsACPBridge:
1387
1396
  self.accumulated_text.clear()
1388
1397
  self.accumulated_thoughts.clear()
1389
1398
  self.accumulated_tool_calls.clear()
1399
+ self._segment_text_start = 0
1400
+ self._segment_thought_start = 0
1401
+ self.text_segments_emitted = 0
1402
+ self.on_text_segment = None
1390
1403
  self.on_token = None
1391
1404
  self.on_event = None
1392
1405
  self.on_activity = None
@@ -1529,6 +1542,7 @@ class _OpenHandsACPBridge:
1529
1542
  if event is not None:
1530
1543
  event.set()
1531
1544
  elif isinstance(update, ToolCallStart):
1545
+ self._emit_text_segment()
1532
1546
  entry = {
1533
1547
  "tool_call_id": update.tool_call_id,
1534
1548
  "title": update.title,
@@ -1599,6 +1613,41 @@ class _OpenHandsACPBridge:
1599
1613
  else:
1600
1614
  logger.debug("ACP session update: %s", type(update).__name__)
1601
1615
 
1616
+ def segment_text(self) -> str:
1617
+ """Text said since the last emitted segment (the whole turn if none)."""
1618
+ return "".join(self.accumulated_text[self._segment_text_start :])
1619
+
1620
+ def segment_thoughts(self) -> str:
1621
+ """Thoughts since the last emitted segment (the whole turn if none)."""
1622
+ return "".join(self.accumulated_thoughts[self._segment_thought_start :])
1623
+
1624
+ def _emit_text_segment(self) -> None:
1625
+ """Hand the text and thoughts said so far to ``on_text_segment``.
1626
+
1627
+ Called when a tool call starts, so that narration such as "let me run
1628
+ the tests" lands before the tool call it announces instead of joining
1629
+ the turn's final message. The joined segment is masked again because a
1630
+ secret split across chunks only reassembles here.
1631
+ """
1632
+ if self.on_text_segment is None:
1633
+ return
1634
+ text = self.segment_text()
1635
+ thoughts = self.segment_thoughts()
1636
+ if not text.strip() and not thoughts.strip():
1637
+ return
1638
+ try:
1639
+ self.on_text_segment(self._mask_value(text), self._mask_value(thoughts))
1640
+ except CredentialBindingError:
1641
+ # Keep the segment: the turn's final text carries it, and the
1642
+ # recorded masking error fails the turn before anything persists.
1643
+ return
1644
+ except Exception:
1645
+ logger.debug("on_text_segment callback failed", exc_info=True)
1646
+ return
1647
+ self._segment_text_start = len(self.accumulated_text)
1648
+ self._segment_thought_start = len(self.accumulated_thoughts)
1649
+ self.text_segments_emitted += 1
1650
+
1602
1651
  def _emit_tool_call_event(self, tc: dict[str, Any]) -> None:
1603
1652
  """Emit an ACPToolCallEvent reflecting the current state of ``tc``.
1604
1653
 
@@ -2805,16 +2854,19 @@ class ACPAgent(AgentBase):
2805
2854
  if self._closed:
2806
2855
  raise CredentialSyncError("Credential binding is closed.")
2807
2856
  try:
2808
- lifecycle.materialize(state.secret_registry, env)
2809
- durable_path = (
2810
- self._acp_file_secret_dir(state, spec.subdir) / spec.filename
2811
- )
2812
- try:
2813
- durable_path.unlink(missing_ok=True)
2814
- except OSError as exc:
2815
- raise CredentialSyncError(
2816
- "Durable credential copy could not be removed."
2817
- ) from exc
2857
+ # The CLI keeps its state in the conversation's durable
2858
+ # directory; the lifecycle links the credential in from a
2859
+ # temporary file, so it never persists there.
2860
+ home = self._acp_file_secret_dir(state, spec.subdir)
2861
+ lifecycle.materialize(state.secret_registry, env, home=home)
2862
+ durable_path = home / spec.filename
2863
+ if lifecycle.path != durable_path:
2864
+ try:
2865
+ durable_path.unlink(missing_ok=True)
2866
+ except OSError as exc:
2867
+ raise CredentialSyncError(
2868
+ "Durable credential copy could not be removed."
2869
+ ) from exc
2818
2870
  except BaseException:
2819
2871
  env.pop("CODEX_HOME", None)
2820
2872
  lifecycle.discard()
@@ -3469,11 +3521,41 @@ class ACPAgent(AgentBase):
3469
3521
  self._client.trace.start_turn(prompt)
3470
3522
  self._client.on_token = on_token
3471
3523
  self._client.on_event = on_event
3524
+ self._client.on_text_segment = functools.partial(
3525
+ self._emit_text_segment, on_event
3526
+ )
3472
3527
  self._client.on_activity = self._on_activity
3473
3528
  # Start the idle-timeout clock fresh for this attempt so the deadline
3474
3529
  # is measured from the send (or retry), not from a stale value.
3475
3530
  self._client.arm_activity_clock()
3476
3531
 
3532
+ def _emit_text_segment(
3533
+ self, on_event: ConversationCallbackType, text: str, thoughts: str
3534
+ ) -> None:
3535
+ """Emit text the agent said before a tool call as its own message.
3536
+
3537
+ It retires the stream slot that carried this text and opens a new one,
3538
+ so the rest of the turn streams below the tool call.
3539
+ """
3540
+ minted: dict[str, Any] = {}
3541
+ if self._stream is not None and (item_id := self._stream.claim()):
3542
+ minted["id"] = item_id
3543
+ on_event(
3544
+ MessageEvent(
3545
+ **minted,
3546
+ source="agent",
3547
+ llm_message=Message(
3548
+ role="assistant",
3549
+ content=[TextContent(text=text)],
3550
+ reasoning_content=thoughts or None,
3551
+ ),
3552
+ )
3553
+ )
3554
+ if self._stream is not None:
3555
+ if minted:
3556
+ self._stream.commit()
3557
+ self._stream.next_item()
3558
+
3477
3559
  def _cancel_inflight_tool_calls(self) -> None:
3478
3560
  """Emit a terminal ``failed`` ACPToolCallEvent for every tool call
3479
3561
  in the accumulator that has not reached a terminal status yet.
@@ -3857,13 +3939,18 @@ class ACPAgent(AgentBase):
3857
3939
  # gets caught before landing in the persisted event stream.
3858
3940
  self._track_file_credentials_for_masking()
3859
3941
  mask = state.secret_registry.mask_secrets_in_output
3860
- response_text = mask("".join(self._client.accumulated_text))
3861
- thought_text = mask("".join(self._client.accumulated_thoughts))
3862
- if not response_text:
3942
+ # Text said before a tool call was already emitted as its own message
3943
+ # (``_emit_text_segment``); the finish carries what the agent said
3944
+ # after its last tool call.
3945
+ response_text = mask(self._client.segment_text())
3946
+ thought_text = mask(self._client.segment_thoughts())
3947
+ if not response_text and not self._client.text_segments_emitted:
3863
3948
  response_text = "(No response from ACP server)"
3864
3949
 
3865
3950
  self._client.trace.finish_turn(
3866
- response_text, thought_text, self._client.accumulated_tool_calls
3951
+ mask("".join(self._client.accumulated_text)) or response_text,
3952
+ mask("".join(self._client.accumulated_thoughts)),
3953
+ self._client.accumulated_tool_calls,
3867
3954
  )
3868
3955
 
3869
3956
  # ACP step() boundaries are full remote assistant turns, not
@@ -4039,6 +4126,7 @@ class ACPAgent(AgentBase):
4039
4126
  return
4040
4127
  self._client.trace.abandon()
4041
4128
  self._client.on_event = None
4129
+ self._client.on_text_segment = None
4042
4130
  self._client.on_token = None
4043
4131
  self._client.on_activity = None
4044
4132
 
@@ -51,7 +51,12 @@ class ACPFileCredentialLifecycle(Protocol):
51
51
  secret_name: str
52
52
  path: Path | None
53
53
 
54
- def materialize(self, registry: SecretRegistry, env: dict[str, str]) -> None: ...
54
+ def materialize(
55
+ self,
56
+ registry: SecretRegistry,
57
+ env: dict[str, str],
58
+ home: Path | None = None,
59
+ ) -> None: ...
55
60
 
56
61
  def track_current(self) -> None: ...
57
62
 
@@ -114,6 +119,7 @@ class _CodexAuthLifecycle:
114
119
  self.run_async = run_async
115
120
  self.path: Path | None = None
116
121
  self._runtime_dir: Path | None = None
122
+ self._home_link: Path | None = None
117
123
  self._registry: SecretRegistry | None = None
118
124
  self._expected_version: str | None = None
119
125
  self._local_digest: str | None = None
@@ -126,7 +132,20 @@ class _CodexAuthLifecycle:
126
132
  self._monitor: threading.Thread | None = None
127
133
  self._closed = False
128
134
 
129
- def materialize(self, registry: SecretRegistry, env: dict[str, str]) -> None:
135
+ def materialize(
136
+ self,
137
+ registry: SecretRegistry,
138
+ env: dict[str, str],
139
+ home: Path | None = None,
140
+ ) -> None:
141
+ """Write the credential to a private temporary file for the CLI.
142
+
143
+ With ``home`` (the conversation's durable Codex directory), ``CODEX_HOME``
144
+ points there so the CLI's threads and state survive a restart, and
145
+ ``home/auth.json`` is a symlink to the temporary file. codex resolves that
146
+ link and replaces its target when it refreshes the login, so the
147
+ credential itself never lands in ``home``.
148
+ """
130
149
  resolved = self._load()
131
150
  if not is_valid_codex_auth(resolved.value):
132
151
  raise CredentialNeedsReauthentication(
@@ -135,8 +154,13 @@ class _CodexAuthLifecycle:
135
154
  runtime_dir = Path(tempfile.mkdtemp(prefix="openhands-codex-"))
136
155
  runtime_dir.chmod(0o700)
137
156
  path = runtime_dir / "auth.json"
157
+ codex_home = runtime_dir
138
158
  try:
139
159
  write_secret_file(path, resolved.value)
160
+ if home is not None:
161
+ link = self._link_into_home(home, path)
162
+ if link is not None:
163
+ path, codex_home = link, home
140
164
  except BaseException:
141
165
  shutil.rmtree(runtime_dir, ignore_errors=True)
142
166
  raise
@@ -146,7 +170,7 @@ class _CodexAuthLifecycle:
146
170
  self._expected_version = resolved.version
147
171
  self._local_digest = self._digest(resolved.value)
148
172
  self._track(resolved.value)
149
- env["CODEX_HOME"] = str(runtime_dir)
173
+ env["CODEX_HOME"] = str(codex_home)
150
174
  monitor = threading.Thread(
151
175
  target=self._monitor_loop,
152
176
  name="codex-credential-monitor",
@@ -164,6 +188,26 @@ class _CodexAuthLifecycle:
164
188
  extra={"credential": self.secret_name},
165
189
  )
166
190
 
191
+ def _link_into_home(self, home: Path, target: Path) -> Path | None:
192
+ link = home / "auth.json"
193
+ try:
194
+ home.mkdir(mode=0o700, parents=True, exist_ok=True)
195
+ # A real file here is a stale copy (older releases, or a CLI that
196
+ # replaced the link): the binding's value supersedes it.
197
+ link.unlink(missing_ok=True)
198
+ link.symlink_to(target)
199
+ except OSError:
200
+ # No symlinks (e.g. Windows without the privilege): keep the CLI's
201
+ # state in the temporary directory, as before.
202
+ logger.warning(
203
+ "credential_binding_home_link_failed",
204
+ extra={"credential": self.secret_name},
205
+ exc_info=True,
206
+ )
207
+ return None
208
+ self._home_link = link
209
+ return link
210
+
167
211
  def track_current(self) -> None:
168
212
  try:
169
213
  self._raise_sticky_error()
@@ -230,12 +274,22 @@ class _CodexAuthLifecycle:
230
274
  self._cleanup_runtime()
231
275
 
232
276
  def _cleanup_runtime(self) -> None:
277
+ home_link = self._home_link
278
+ if home_link is not None:
279
+ try:
280
+ home_link.unlink(missing_ok=True)
281
+ except OSError:
282
+ logger.warning(
283
+ "credential_binding_home_link_cleanup_failed",
284
+ extra={"credential": self.secret_name},
285
+ )
233
286
  runtime_dir = self._runtime_dir
234
287
  if runtime_dir is not None:
235
288
  shutil.rmtree(runtime_dir, ignore_errors=True)
236
289
  with self._lock:
237
290
  self.path = None
238
291
  self._runtime_dir = None
292
+ self._home_link = None
239
293
  self._registry = None
240
294
  self._monitor = None
241
295
  self._closed = True
@@ -51,6 +51,7 @@ from openhands.sdk.event.condenser import (
51
51
  CondensationRequest,
52
52
  )
53
53
  from openhands.sdk.event.error_classification import AGENT_OUTCOME
54
+ from openhands.sdk.event.llm_convertible.observation import ToolValidationError
54
55
  from openhands.sdk.event.security import SecurityAnalysisEvent
55
56
  from openhands.sdk.event.types import ROOT_PARENT_ID, EventID
56
57
  from openhands.sdk.llm import (
@@ -1322,6 +1323,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1322
1323
  thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] | None = None,
1323
1324
  responses_reasoning_item: ReasoningItemModel | None = None,
1324
1325
  stream: StreamContext | None = None,
1326
+ validation_errors: list[ToolValidationError] | None = None,
1325
1327
  ) -> None:
1326
1328
  try:
1327
1329
  json.loads(tool_call.arguments)
@@ -1361,6 +1363,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1361
1363
  tool_name=tool_name,
1362
1364
  tool_call_id=tool_call.id,
1363
1365
  classification=AGENT_OUTCOME,
1366
+ validation_errors=validation_errors,
1364
1367
  )
1365
1368
  record_tool_result(
1366
1369
  conversation,
@@ -1489,6 +1492,18 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1489
1492
  thinking_blocks=thinking_blocks,
1490
1493
  responses_reasoning_item=responses_reasoning_item,
1491
1494
  stream=stream,
1495
+ validation_errors=(
1496
+ [
1497
+ ToolValidationError(type=detail["type"], loc=detail["loc"])
1498
+ for detail in e.errors(
1499
+ include_url=False,
1500
+ include_context=False,
1501
+ include_input=False,
1502
+ )
1503
+ ]
1504
+ if isinstance(e, ValidationError)
1505
+ else None
1506
+ ),
1492
1507
  )
1493
1508
  return
1494
1509
 
@@ -28,6 +28,9 @@ from openhands.sdk.context.prompts.section import Platform, PromptContext
28
28
  from openhands.sdk.critic.base import CriticBase
29
29
  from openhands.sdk.llm import LLM
30
30
  from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
31
+ from openhands.sdk.llm.utils.openhands_provider import (
32
+ OPENHANDS_SYSTEM_IDENTITY,
33
+ )
31
34
  from openhands.sdk.logger import get_logger
32
35
  from openhands.sdk.mcp.client import MCPClient
33
36
  from openhands.sdk.mcp.config import MCPServer
@@ -64,10 +67,7 @@ logger = get_logger(__name__)
64
67
  # replaces the default identity in the system prompt.
65
68
 
66
69
  _SOUL_PATH = get_user_persistence_dir() / "SOUL.md"
67
- _DEFAULT_SOUL = (
68
- "You are OpenHands agent, a helpful AI assistant that can interact"
69
- " with a computer to solve tasks."
70
- )
70
+ _DEFAULT_SOUL = OPENHANDS_SYSTEM_IDENTITY
71
71
 
72
72
  # Built-in prompt dir. The registry only stands in for built-in prompts here; a
73
73
  # subclass with its own prompts/ keeps the Jinja render path.
@@ -162,12 +162,14 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
162
162
  default_factory=lambda: [tool.__name__ for tool in BUILT_IN_TOOLS],
163
163
  description=(
164
164
  "List of default tool class names to include. By default, the agent "
165
- "includes 'FinishTool' and 'ThinkTool'. Set to an empty list to disable "
166
- "all default tools, or provide a subset to include only specific ones. "
167
- "Example: include_default_tools=['FinishTool'] to only include FinishTool, "
168
- "or include_default_tools=[] to disable all default tools."
165
+ "includes 'FinishTool'. Set to an empty list to disable all default "
166
+ "tools, or provide a subset to include specific ones. Add 'ThinkTool' "
167
+ "explicitly to enable the think tool. "
168
+ "Example: include_default_tools=['FinishTool', 'ThinkTool'] to enable "
169
+ "the think tool, or include_default_tools=[] to disable all default "
170
+ "tools."
169
171
  ),
170
- examples=[["FinishTool", "ThinkTool"], ["FinishTool"], []],
172
+ examples=[["FinishTool"], ["FinishTool", "ThinkTool"], []],
171
173
  )
172
174
  agent_context: AgentContext | None = Field(
173
175
  default=None,
@@ -82,9 +82,11 @@ class StreamContext:
82
82
  on_token: Callable[[Any], None] | None,
83
83
  on_stream: StreamProgressCallbackType | None,
84
84
  mask: Callable[[], StreamOutputMask],
85
+ last_seq: Callable[[], int | None] | None = None,
85
86
  ) -> None:
86
87
  self.item_id = item_id
87
88
  self._anchor_seq = anchor_seq
89
+ self._last_seq = last_seq
88
90
  self._on_token = on_token
89
91
  self._on_stream = on_stream
90
92
  self._mask = mask
@@ -99,6 +101,7 @@ class StreamContext:
99
101
  self._reserved = False
100
102
  self._claimed = False
101
103
  self._chunk_id: str | None = None
104
+ self._anchor_pending = False
102
105
 
103
106
  @classmethod
104
107
  def open(
@@ -108,13 +111,18 @@ class StreamContext:
108
111
  ) -> StreamContext:
109
112
  """Mint an id and read the anchor. Neither touches the event log."""
110
113
  state = conversation.state
111
- length = len(state.events)
114
+
115
+ def last_seq() -> int | None:
116
+ length = len(state.events)
117
+ return length - 1 if length else None
118
+
112
119
  return cls(
113
120
  item_id=str(uuid.uuid4()),
114
- anchor_seq=length - 1 if length else None,
121
+ anchor_seq=last_seq(),
115
122
  on_token=on_token,
116
123
  on_stream=conversation.on_stream,
117
124
  mask=state.secret_registry.compile_stream_mask,
125
+ last_seq=last_seq,
118
126
  )
119
127
 
120
128
  @property
@@ -176,6 +184,28 @@ class StreamContext:
176
184
  if self._reserved:
177
185
  self._claimed = True
178
186
 
187
+ def next_item(self) -> None:
188
+ """Open a fresh slot for the rest of the step, after a committed item.
189
+
190
+ For a step that emits several durable items in turn (an ACP turn's text
191
+ segments between its tool calls). The new slot anchors on the event log
192
+ as it stands when its first token arrives, so it streams below every
193
+ event emitted in between.
194
+ """
195
+ self.close("no_durable_event")
196
+ self.item_id = str(uuid.uuid4())
197
+ self._anchor_seq = None
198
+ self._masks.clear()
199
+ self._attempt = 1
200
+ self._order = itertools.count()
201
+ self._started = False
202
+ self._opened = False
203
+ self._started_attempt = None
204
+ self._reserved = False
205
+ self._claimed = False
206
+ self._chunk_id = None
207
+ self._anchor_pending = self._last_seq is not None
208
+
179
209
  def close(self, reason: str) -> None:
180
210
  """Emit ``StreamAborted`` unless a durable event committed the id.
181
211
 
@@ -211,6 +241,9 @@ class StreamContext:
211
241
  self.new_attempt()
212
242
  self._chunk_id = chunk_id
213
243
  if not self._started:
244
+ if self._anchor_pending and self._last_seq is not None:
245
+ self._anchor_seq = self._last_seq()
246
+ self._anchor_pending = False
214
247
  self._started = self._opened = True
215
248
  self._started_attempt = self._attempt
216
249
  self._emit(StreamStarted(self.item_id, self._attempt, self._anchor_seq))
@@ -26,7 +26,11 @@ from openhands.sdk.context.condenser.base import CondenserBase
26
26
  from openhands.sdk.context.view import View
27
27
  from openhands.sdk.event.base import LLMConvertibleEvent
28
28
  from openhands.sdk.event.condenser import Condensation
29
- from openhands.sdk.llm import LLM, Message
29
+ from openhands.sdk.llm import LLM, Message, TextContent
30
+ from openhands.sdk.llm.mixins.fn_call_converter import (
31
+ TERMINAL_COMMAND_PREFIX_ALIASES,
32
+ TOOL_NAME_ALIASES,
33
+ )
30
34
 
31
35
 
32
36
  # Regex matching raw ASCII control characters (U+0000–U+001F) that are
@@ -240,17 +244,6 @@ def fix_malformed_tool_arguments(
240
244
  return fixed_arguments
241
245
 
242
246
 
243
- TOOL_NAME_ALIASES: dict[str, str] = {
244
- "bash": "terminal",
245
- "command": "terminal",
246
- "execute": "terminal",
247
- "execute_bash": "terminal",
248
- "git": "terminal",
249
- "reset": "terminal",
250
- "str_replace": "file_editor",
251
- "str_replace_editor": "file_editor",
252
- }
253
-
254
247
  # Regex to detect malformed tool names (e.g., "str_replace </parameter"
255
248
  # or "str_replace</function>"). These occur when LLMs emit XML/HTML
256
249
  # tag fragments in tool names. The leading identifier is extracted and
@@ -270,12 +263,6 @@ def _extract_tool_name_base(tool_name: str) -> str:
270
263
  return match.group(1) if match else tool_name
271
264
 
272
265
 
273
- # Terminal aliases that prepend the tool name to the command argument.
274
- # Unlike 'bash' which passes through the command directly, these tools
275
- # (e.g., 'git', 'reset') are themselves commands that should be combined
276
- # with their arguments (e.g., 'git status', 'reset clear').
277
- _TERMINAL_COMMAND_PREFIX_ALIASES = frozenset({"git", "reset"})
278
-
279
266
  # This fallback is intentionally tiny: it only accepts exact, bare command names
280
267
  # that are useful as read-only defaults when some models emit them as tool names.
281
268
  _SHELL_TOOL_FALLBACK_COMMANDS = frozenset({"find", "git", "ls", "pwd"})
@@ -506,7 +493,7 @@ def normalize_tool_call(
506
493
  # For terminal alias with prefix, combine tool name with command
507
494
  if (
508
495
  alias_target == "terminal"
509
- and base_name in _TERMINAL_COMMAND_PREFIX_ALIASES
496
+ and base_name in TERMINAL_COMMAND_PREFIX_ALIASES
510
497
  ):
511
498
  original_command = arguments.get("command")
512
499
  normalized_arguments = {
@@ -560,6 +547,34 @@ def normalize_tool_call(
560
547
  return normalized_tool_name, normalized_arguments
561
548
 
562
549
 
550
+ _TRAILING_ASSISTANT_NUDGE = (
551
+ "Your previous response has already been delivered to the user. If the user "
552
+ "sent a new message above that your previous response did not address, "
553
+ "respond to it now. Otherwise, continue the task."
554
+ )
555
+
556
+
557
+ def _ensure_trailing_user_message(messages: list[Message]) -> None:
558
+ """Append a request-only user turn when the history ends with the assistant.
559
+
560
+ A user message that lands while the LLM call is in flight is recorded before
561
+ the agent's reply (``astep`` releases the state lock for the network wait),
562
+ so the continued run would otherwise send ``[..., user, assistant]``. Chat
563
+ APIs expect the final turn to be a user turn, and Anthropic rejects a
564
+ trailing assistant message on models without prefill support. The nudge is
565
+ not persisted: it only shapes this request.
566
+ """
567
+ if not messages or messages[-1].role != "assistant":
568
+ return
569
+ logger.warning(
570
+ "LLM history ends with an assistant message; appending a user turn so "
571
+ "the request does not end with assistant prefill."
572
+ )
573
+ messages.append(
574
+ Message(role="user", content=[TextContent(text=_TRAILING_ASSISTANT_NUDGE)])
575
+ )
576
+
577
+
563
578
  @overload
564
579
  def prepare_llm_messages(
565
580
  view: View,
@@ -630,6 +645,7 @@ def prepare_llm_messages(
630
645
  if additional_messages:
631
646
  messages.extend(additional_messages)
632
647
 
648
+ _ensure_trailing_user_message(messages)
633
649
  return messages
634
650
 
635
651
 
@@ -665,4 +681,5 @@ async def aprepare_llm_messages(
665
681
  if additional_messages:
666
682
  messages.extend(additional_messages)
667
683
 
684
+ _ensure_trailing_user_message(messages)
668
685
  return messages
@@ -19,6 +19,9 @@ from openhands.sdk.context.prompts.section import (
19
19
  Platform,
20
20
  PromptContext,
21
21
  )
22
+ from openhands.sdk.llm.utils.openhands_provider import (
23
+ OPENHANDS_SYSTEM_IDENTITY,
24
+ )
22
25
  from openhands.sdk.utils.path import get_user_persistence_dir, to_posix_path
23
26
 
24
27
 
@@ -81,10 +84,7 @@ class _StaticTextSection:
81
84
  class SoulSection(_StaticTextSection):
82
85
  name = "soul"
83
86
 
84
- _DEFAULT_SOUL = (
85
- "You are OpenHands agent, a helpful AI assistant that can interact"
86
- " with a computer to solve tasks."
87
- )
87
+ _DEFAULT_SOUL = OPENHANDS_SYSTEM_IDENTITY
88
88
 
89
89
  def render(self, ctx: PromptContext) -> str | None:
90
90
  soul = str(ctx.template_kwargs.get("soul_content") or self._DEFAULT_SOUL)
@@ -258,6 +258,13 @@ class BaseConversation(ABC):
258
258
 
259
259
  This method runs the agent until it finishes processing the current
260
260
  message or reaches the maximum iteration limit.
261
+
262
+ When the run ends because the iteration limit is reached
263
+ (``ERROR``) or the agent is stuck (``STUCK``), the implementations
264
+ differ: ``LocalConversation.run()`` returns and leaves that status in
265
+ ``state.execution_status``, while ``RemoteConversation.run()`` raises
266
+ :class:`ConversationRunError`. Errors raised during the run, such as
267
+ LLM errors, raise ``ConversationRunError`` in both.
261
268
  """
262
269
  ...
263
270
 
@@ -472,10 +479,10 @@ class BaseConversation(ABC):
472
479
  if ``None``).
473
480
  agent: Agent for the fork. Defaults to a deep-copy of the
474
481
  source agent.
475
- title: Deprecated on ``LocalConversation`` since 1.54.0 — has no
476
- effect there and emits a ``DeprecationWarning``. Still
477
- honoured by ``RemoteConversation`` (passed to the agent
478
- server which stores it on ``StoredConversation``).
482
+ title: Honoured by ``RemoteConversation`` (passed to the agent
483
+ server which stores it on ``StoredConversation``); accepted
484
+ but ignored by ``LocalConversation``, which has no title
485
+ field.
479
486
  tags: Optional tags for the forked conversation.
480
487
  reset_metrics: If ``True`` (default), cost/token stats start
481
488
  fresh on the fork.
@@ -339,11 +339,7 @@ class EventLog(EventsListBase):
339
339
  except Exception as e:
340
340
  logger.warning("Error listing event directory %s: %s", self._dir, e)
341
341
  return 0
342
- return sum(
343
- 1
344
- for p in paths
345
- if posix_path_name(p).startswith("event-") and p.endswith(".json")
346
- )
342
+ return sum(1 for p in paths if EVENT_NAME_RE.match(posix_path_name(p)))
347
343
 
348
344
  def _sync_from_disk(self, disk_length: int) -> None:
349
345
  """Sync state for events written by other processes.