openhands-sdk 1.58.2__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 (332) hide show
  1. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/PKG-INFO +4 -4
  2. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/__init__.py +4 -14
  3. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_agent.py +152 -16
  4. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_file_credentials.py +57 -3
  5. openhands_sdk-1.60.0/openhands/sdk/agent/acp_skills.py +67 -0
  6. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/agent.py +53 -2
  7. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/base.py +11 -9
  8. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/stream_context.py +35 -2
  9. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/utils.py +36 -19
  10. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/static.py +4 -4
  11. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/base.py +11 -4
  12. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/conversation.py +5 -0
  13. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/event_store.py +132 -57
  14. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/exceptions.py +25 -0
  15. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/local_conversation.py +179 -56
  16. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/remote_conversation.py +64 -24
  17. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/request.py +18 -6
  18. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/response_utils.py +11 -1
  19. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/secret_registry.py +13 -11
  20. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/state.py +36 -0
  21. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/stuck_detector.py +163 -18
  22. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/title_utils.py +5 -11
  23. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/taxonomy.py +4 -1
  24. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/__init__.py +2 -1
  25. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/acp_tool_call.py +2 -2
  26. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/base.py +3 -2
  27. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/hook_execution.py +3 -3
  28. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_completion_log.py +2 -2
  29. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/observation.py +14 -1
  30. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/types.py +13 -3
  31. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/fetch.py +21 -12
  32. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/manager.py +17 -7
  33. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/git_changes.py +41 -92
  34. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/git_commits.py +3 -2
  35. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/utils.py +11 -1
  36. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/io/memory.py +11 -3
  37. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/launch/finalize.py +4 -2
  38. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/launch/preview.py +1 -0
  39. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/launch/resolve.py +1 -0
  40. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/__init__.py +29 -0
  41. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/openai.py +4 -4
  42. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/call_context.py +8 -0
  43. openhands_sdk-1.60.0/openhands/sdk/llm/decision.py +580 -0
  44. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/classifier.py +31 -5
  45. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/mapping.py +7 -0
  46. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm.py +60 -11
  47. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_profile_store.py +1 -1
  48. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/message.py +11 -12
  49. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/fn_call_converter.py +20 -5
  50. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/responses_options.py +1 -0
  51. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_features.py +79 -6
  52. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_info.py +32 -10
  53. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/openhands_provider.py +16 -0
  54. openhands_sdk-1.60.0/openhands/sdk/llm/utils/retry_after.py +67 -0
  55. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/retry_mixin.py +38 -6
  56. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/verified_models.py +16 -4
  57. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/tool.py +27 -1
  58. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/__init__.py +0 -6
  59. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/agent_profile.py +24 -1
  60. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/resolver.py +7 -215
  61. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/seed.py +1 -1
  62. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/analyzer.py +7 -1
  63. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/__init__.py +0 -6
  64. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/acp_install_catalog.py +1 -1
  65. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/acp_providers.py +0 -209
  66. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/model.py +47 -7
  67. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/skill.py +4 -0
  68. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/utils.py +14 -1
  69. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/subagent/registry.py +24 -5
  70. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/subagent/schema.py +4 -0
  71. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/__init__.py +9 -4
  72. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/schema.py +8 -1
  73. openhands_sdk-1.60.0/openhands/sdk/utils/masking.py +70 -0
  74. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/redact.py +3 -1
  75. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/base.py +2 -2
  76. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/repo.py +99 -10
  77. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/PKG-INFO +4 -4
  78. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/SOURCES.txt +6 -0
  79. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/requires.txt +3 -3
  80. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/pyproject.toml +4 -4
  81. openhands_sdk-1.58.2/openhands/sdk/utils/masking.py +0 -13
  82. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/__init__.py +0 -0
  83. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  84. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  85. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_models.py +0 -0
  86. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  87. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  88. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  89. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  90. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/automation/__init__.py +0 -0
  91. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/banner.py +0 -0
  92. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/__init__.py +0 -0
  93. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/agent_context.py +0 -0
  94. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  95. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/base.py +0 -0
  96. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  97. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  98. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  99. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  100. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  101. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/condenser/utils.py +0 -0
  102. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/memory.py +0 -0
  103. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  104. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/presets.py +0 -0
  105. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  106. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/registry.py +0 -0
  107. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/section.py +0 -0
  108. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  109. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  110. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  111. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  112. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  113. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/__init__.py +0 -0
  114. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  115. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  116. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/base.py +0 -0
  117. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  118. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  119. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  120. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  121. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/context/view/view.py +0 -0
  122. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/__init__.py +0 -0
  123. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/cancellation.py +0 -0
  124. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  125. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  126. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  127. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  128. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/controller.py +0 -0
  129. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  130. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  131. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  132. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  133. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/message_request.py +0 -0
  134. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  135. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  136. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/types.py +0 -0
  137. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  138. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  139. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  140. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/credential.py +0 -0
  141. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/__init__.py +0 -0
  142. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/base.py +0 -0
  143. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  144. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  145. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  146. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  147. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  148. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  149. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  150. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  151. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/critic/result.py +0 -0
  152. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/condenser.py +0 -0
  153. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/conversation_error.py +0 -0
  154. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/conversation_state.py +0 -0
  155. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/error_classification.py +0 -0
  156. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  157. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  158. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  159. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  160. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  161. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/resume_transcript.py +0 -0
  162. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/security.py +0 -0
  163. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/streaming_delta.py +0 -0
  164. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/token.py +0 -0
  165. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/event/user_action.py +0 -0
  166. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/__init__.py +0 -0
  167. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  168. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/info.py +0 -0
  169. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  170. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  171. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  172. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/cached_repo.py +0 -0
  173. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/exceptions.py +0 -0
  174. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/git_diff.py +0 -0
  175. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/git/models.py +0 -0
  176. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/__init__.py +0 -0
  177. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/config.py +0 -0
  178. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  179. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/executor.py +0 -0
  180. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/manager.py +0 -0
  181. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/hooks/types.py +0 -0
  182. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/io/__init__.py +0 -0
  183. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/io/base.py +0 -0
  184. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/io/cache.py +0 -0
  185. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/io/local.py +0 -0
  186. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/launch/__init__.py +0 -0
  187. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/launch/errors.py +0 -0
  188. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  189. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/_response_stream.py +0 -0
  190. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  191. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  192. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  193. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  194. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  195. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  196. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  197. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_registry.py +0 -0
  198. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/llm_response.py +0 -0
  199. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/meta_profile_store.py +0 -0
  200. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  201. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  202. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/__init__.py +0 -0
  203. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  204. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/options/common.py +0 -0
  205. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  206. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/__init__.py +0 -0
  207. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/base.py +0 -0
  208. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  209. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  210. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/streaming.py +0 -0
  211. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  212. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  213. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  214. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  215. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  216. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  217. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  218. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  219. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  220. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  221. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  222. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  223. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/logger/__init__.py +0 -0
  224. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/logger/_rich_handler.py +0 -0
  225. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/logger/logger.py +0 -0
  226. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/logger/rolling.py +0 -0
  227. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/__init__.py +0 -0
  228. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/registration.py +0 -0
  229. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/registry.py +0 -0
  230. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/marketplace/types.py +0 -0
  231. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/__init__.py +0 -0
  232. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/client.py +0 -0
  233. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/config.py +0 -0
  234. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/definition.py +0 -0
  235. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/exceptions.py +0 -0
  236. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/oauth.py +0 -0
  237. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/mcp/utils.py +0 -0
  238. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/observability/__init__.py +0 -0
  239. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/observability/laminar.py +0 -0
  240. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/observability/utils.py +0 -0
  241. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/__init__.py +0 -0
  242. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/discovery.py +0 -0
  243. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/fetch.py +0 -0
  244. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  245. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
  246. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  247. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/base.py +0 -0
  248. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  249. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  250. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  251. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/installed.py +0 -0
  252. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/loader.py +0 -0
  253. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/paths.py +0 -0
  254. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/plugin.py +0 -0
  255. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/source.py +0 -0
  256. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/strict_fetch.py +0 -0
  257. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/plugin/types.py +0 -0
  258. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/agent_profile_store.py +0 -0
  259. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/profiles/profile_refs.py +0 -0
  260. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/py.typed +0 -0
  261. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/secret/__init__.py +0 -0
  262. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/secret/secrets.py +0 -0
  263. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/__init__.py +0 -0
  264. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/_shell_ast.py +0 -0
  265. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/analyzer.py +0 -0
  266. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  267. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  268. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  269. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  270. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
  271. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  272. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/ensemble.py +0 -0
  273. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  274. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  275. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  276. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/risk.py +0 -0
  277. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/shell_parser.py +0 -0
  278. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  279. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  280. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/api_models.py +0 -0
  281. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/settings/metadata.py +0 -0
  282. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/__init__.py +0 -0
  283. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/exceptions.py +0 -0
  284. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/execute.py +0 -0
  285. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/fetch.py +0 -0
  286. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/installed.py +0 -0
  287. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/trigger.py +0 -0
  288. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/skills/types.py +0 -0
  289. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/subagent/__init__.py +0 -0
  290. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/subagent/load.py +0 -0
  291. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/subagent/scope.py +0 -0
  292. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/testing/__init__.py +0 -0
  293. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/testing/test_llm.py +0 -0
  294. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/__init__.py +0 -0
  295. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  296. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  297. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  298. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  299. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/think.py +0 -0
  300. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  301. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/client_tool.py +0 -0
  302. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/defaults.py +0 -0
  303. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/registry.py +0 -0
  304. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/spec.py +0 -0
  305. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/tool/tool.py +0 -0
  306. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/__init__.py +0 -0
  307. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/async_executor.py +0 -0
  308. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/async_utils.py +0 -0
  309. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/cipher.py +0 -0
  310. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/command.py +0 -0
  311. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/datetime.py +0 -0
  312. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/deprecation.py +0 -0
  313. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/files.py +0 -0
  314. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/github.py +0 -0
  315. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/json.py +0 -0
  316. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/models.py +0 -0
  317. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/paging.py +0 -0
  318. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/path.py +0 -0
  319. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  320. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/truncate.py +0 -0
  321. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/utils/visualize.py +0 -0
  322. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/__init__.py +0 -0
  323. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/base.py +0 -0
  324. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/local.py +0 -0
  325. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/models.py +0 -0
  326. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  327. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  328. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  329. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands/sdk/workspace/workspace.py +0 -0
  330. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  331. {openhands_sdk-1.58.2 → openhands_sdk-1.60.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  332. {openhands_sdk-1.58.2 → 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.58.2
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"
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from importlib.metadata import PackageNotFoundError, version
4
- from typing import TYPE_CHECKING, Any
5
4
 
6
5
  from openhands.sdk.agent import (
7
6
  Agent,
@@ -27,6 +26,8 @@ from openhands.sdk.io import FileStore, LocalFileStore
27
26
  from openhands.sdk.llm import (
28
27
  LLM,
29
28
  LLM_PROFILE_SCHEMA_VERSION,
29
+ DecisionModel,
30
+ DecisionResult,
30
31
  FallbackStrategy,
31
32
  ImageContent,
32
33
  LLMProfileStore,
@@ -109,10 +110,6 @@ from openhands.sdk.workspace import (
109
110
  )
110
111
 
111
112
 
112
- if TYPE_CHECKING:
113
- from openhands.sdk.settings.acp_providers import ACPModelOption
114
-
115
-
116
113
  try:
117
114
  __version__ = version("openhands-sdk")
118
115
  except PackageNotFoundError:
@@ -124,6 +121,8 @@ _print_banner(__version__)
124
121
 
125
122
  __all__ = [
126
123
  "LLM",
124
+ "DecisionModel",
125
+ "DecisionResult",
127
126
  "LLM_PROFILE_SCHEMA_VERSION",
128
127
  "LLMRegistry",
129
128
  "LLMProfileStore",
@@ -167,7 +166,6 @@ __all__ = [
167
166
  "ACP_PROVIDERS",
168
167
  "ACPAgentSettings",
169
168
  "ACPFileSecretSpec",
170
- "ACPModelOption",
171
169
  "ACPProviderInfo",
172
170
  "AgentSettingsBase",
173
171
  "AgentSettingsConfig",
@@ -211,11 +209,3 @@ __all__ = [
211
209
  "page_iterator",
212
210
  "__version__",
213
211
  ]
214
-
215
-
216
- def __getattr__(name: str) -> Any:
217
- if name == "ACPModelOption":
218
- from openhands.sdk.settings.acp_providers import _deprecated_model_option
219
-
220
- return _deprecated_model_option()
221
- raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
@@ -19,10 +19,12 @@ 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
25
26
  import re
27
+ import shutil
26
28
  import threading
27
29
  import time
28
30
  import uuid
@@ -106,6 +108,7 @@ from openhands.sdk.agent.acp_file_credentials import (
106
108
  write_secret_file,
107
109
  )
108
110
  from openhands.sdk.agent.acp_models import ACPModelInfo
111
+ from openhands.sdk.agent.acp_skills import render_skill_catalog, write_skill_files
109
112
  from openhands.sdk.agent.acp_tracing import ACPTurnTrace
110
113
  from openhands.sdk.agent.base import AgentBase
111
114
  from openhands.sdk.agent.stream_context import StreamContext
@@ -1340,6 +1343,14 @@ class _OpenHandsACPBridge:
1340
1343
  self.accumulated_text: list[str] = []
1341
1344
  self.accumulated_thoughts: list[str] = []
1342
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
1343
1354
  # Emits the LLM/TOOL spans an ACP turn would otherwise never produce.
1344
1355
  self.trace = ACPTurnTrace(acp_server=None, model_id=None)
1345
1356
  self.on_token: Any = None # ConversationTokenCallbackType | None
@@ -1385,6 +1396,10 @@ class _OpenHandsACPBridge:
1385
1396
  self.accumulated_text.clear()
1386
1397
  self.accumulated_thoughts.clear()
1387
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
1388
1403
  self.on_token = None
1389
1404
  self.on_event = None
1390
1405
  self.on_activity = None
@@ -1527,6 +1542,7 @@ class _OpenHandsACPBridge:
1527
1542
  if event is not None:
1528
1543
  event.set()
1529
1544
  elif isinstance(update, ToolCallStart):
1545
+ self._emit_text_segment()
1530
1546
  entry = {
1531
1547
  "tool_call_id": update.tool_call_id,
1532
1548
  "title": update.title,
@@ -1597,6 +1613,41 @@ class _OpenHandsACPBridge:
1597
1613
  else:
1598
1614
  logger.debug("ACP session update: %s", type(update).__name__)
1599
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
+
1600
1651
  def _emit_tool_call_event(self, tc: dict[str, Any]) -> None:
1601
1652
  """Emit an ACPToolCallEvent reflecting the current state of ``tc``.
1602
1653
 
@@ -2020,6 +2071,10 @@ class ACPAgent(AgentBase):
2020
2071
  # "installed" — already in subprocess history; skip further injection
2021
2072
  _suffix_install_state: str = PrivateAttr(default="unused")
2022
2073
  _installed_suffix: str | None = PrivateAttr(default=None)
2074
+ # Skills written to disk and listed by path in the prompt, not inlined.
2075
+ _skill_locations: dict[str, Path] = PrivateAttr(default_factory=dict)
2076
+ _skills_root: Path | None = PrivateAttr(default=None)
2077
+ _additional_directories_supported: bool = PrivateAttr(default=False)
2023
2078
  _restart_session_on_next_turn: bool = PrivateAttr(default=False)
2024
2079
  # Stream identity for the turn in flight; see stream_context.py. Held on
2025
2080
  # the agent rather than threaded through the finalizers because the ACP
@@ -2331,6 +2386,7 @@ class ACPAgent(AgentBase):
2331
2386
 
2332
2387
  # Render the suffix once, pulling secrets from the conversation's
2333
2388
  # secret_registry to match the regular Agent's get_dynamic_context().
2389
+ self._materialize_skills(state)
2334
2390
  self._installed_suffix = self._render_suffix(state)
2335
2391
  # A prior session id means we may be resuming; used by ``truly_resumed``
2336
2392
  # below to decide whether the model state reported for this launch
@@ -2493,6 +2549,25 @@ class ACPAgent(AgentBase):
2493
2549
  )
2494
2550
  )
2495
2551
 
2552
+ def _skill_directories(self) -> list[str] | None:
2553
+ if self._skills_root and self._additional_directories_supported:
2554
+ return [str(self._skills_root)]
2555
+ return None
2556
+
2557
+ def _materialize_skills(self, state: ConversationState) -> None:
2558
+ self._skill_locations = {}
2559
+ self._skills_root = None
2560
+ context = self.agent_context
2561
+ if context is None or not context.skills:
2562
+ return
2563
+ _, available = context._partition_skills()
2564
+ if not available:
2565
+ return
2566
+ root = self._acp_file_secret_dir(state, "skills")
2567
+ shutil.rmtree(root, ignore_errors=True)
2568
+ self._skill_locations = write_skill_files(root, available)
2569
+ self._skills_root = root
2570
+
2496
2571
  def _render_suffix(self, state: ConversationState) -> str | None:
2497
2572
  """Render the system suffix once, including secrets from the registry.
2498
2573
 
@@ -2533,12 +2608,23 @@ class ACPAgent(AgentBase):
2533
2608
  if not secret_infos:
2534
2609
  return None
2535
2610
  agent_context = AgentContext(current_datetime=None)
2536
- elif agent_context.secrets:
2611
+ listed = [s for s in agent_context.skills if s.name in self._skill_locations]
2612
+ if listed:
2613
+ agent_context = agent_context.model_copy(
2614
+ update={"skills": [s for s in agent_context.skills if s not in listed]}
2615
+ )
2616
+ if agent_context.secrets:
2537
2617
  # The registry already carries these (and their descriptions), so
2538
2618
  # clear the agent_context copy to advertise from the registry alone
2539
2619
  # rather than re-merging a redundant second source.
2540
2620
  agent_context = agent_context.model_copy(update={"secrets": {}})
2541
- return agent_context.to_acp_prompt_context(additional_secret_infos=secret_infos)
2621
+ rendered = agent_context.to_acp_prompt_context(
2622
+ additional_secret_infos=secret_infos
2623
+ )
2624
+ if not listed:
2625
+ return rendered
2626
+ catalog = render_skill_catalog(listed, self._skill_locations)
2627
+ return f"{rendered}\n\n{catalog}" if rendered else catalog
2542
2628
 
2543
2629
  def _present_file_secret_names(self, state: ConversationState) -> set[str]:
2544
2630
  """Reserved file-content secret names supplied for this conversation.
@@ -2768,16 +2854,19 @@ class ACPAgent(AgentBase):
2768
2854
  if self._closed:
2769
2855
  raise CredentialSyncError("Credential binding is closed.")
2770
2856
  try:
2771
- lifecycle.materialize(state.secret_registry, env)
2772
- durable_path = (
2773
- self._acp_file_secret_dir(state, spec.subdir) / spec.filename
2774
- )
2775
- try:
2776
- durable_path.unlink(missing_ok=True)
2777
- except OSError as exc:
2778
- raise CredentialSyncError(
2779
- "Durable credential copy could not be removed."
2780
- ) 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
2781
2870
  except BaseException:
2782
2871
  env.pop("CODEX_HOME", None)
2783
2872
  lifecycle.discard()
@@ -3161,6 +3250,15 @@ class ACPAgent(AgentBase):
3161
3250
  if init_response.agent_capabilities is not None
3162
3251
  else None
3163
3252
  )
3253
+ session_caps = (
3254
+ init_response.agent_capabilities.session_capabilities
3255
+ if init_response.agent_capabilities is not None
3256
+ else None
3257
+ )
3258
+ self._additional_directories_supported = (
3259
+ session_caps is not None
3260
+ and session_caps.additional_directories is not None
3261
+ )
3164
3262
  acp_mcp_servers = _mcp_config_to_acp_servers(self.mcp_config, mcp_caps)
3165
3263
  if acp_mcp_servers:
3166
3264
  logger.info(
@@ -3256,6 +3354,7 @@ class ACPAgent(AgentBase):
3256
3354
  load_response = await conn.load_session(
3257
3355
  cwd=working_dir,
3258
3356
  session_id=prior_session_id,
3357
+ additional_directories=self._skill_directories(),
3259
3358
  mcp_servers=acp_mcp_servers,
3260
3359
  )
3261
3360
  session_id = prior_session_id
@@ -3301,6 +3400,7 @@ class ACPAgent(AgentBase):
3301
3400
  session_meta = build_session_model_meta(agent_name, self.acp_model)
3302
3401
  response = await conn.new_session(
3303
3402
  cwd=working_dir,
3403
+ additional_directories=self._skill_directories(),
3304
3404
  mcp_servers=acp_mcp_servers,
3305
3405
  **session_meta,
3306
3406
  )
@@ -3421,11 +3521,41 @@ class ACPAgent(AgentBase):
3421
3521
  self._client.trace.start_turn(prompt)
3422
3522
  self._client.on_token = on_token
3423
3523
  self._client.on_event = on_event
3524
+ self._client.on_text_segment = functools.partial(
3525
+ self._emit_text_segment, on_event
3526
+ )
3424
3527
  self._client.on_activity = self._on_activity
3425
3528
  # Start the idle-timeout clock fresh for this attempt so the deadline
3426
3529
  # is measured from the send (or retry), not from a stale value.
3427
3530
  self._client.arm_activity_clock()
3428
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
+
3429
3559
  def _cancel_inflight_tool_calls(self) -> None:
3430
3560
  """Emit a terminal ``failed`` ACPToolCallEvent for every tool call
3431
3561
  in the accumulator that has not reached a terminal status yet.
@@ -3809,13 +3939,18 @@ class ACPAgent(AgentBase):
3809
3939
  # gets caught before landing in the persisted event stream.
3810
3940
  self._track_file_credentials_for_masking()
3811
3941
  mask = state.secret_registry.mask_secrets_in_output
3812
- response_text = mask("".join(self._client.accumulated_text))
3813
- thought_text = mask("".join(self._client.accumulated_thoughts))
3814
- 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:
3815
3948
  response_text = "(No response from ACP server)"
3816
3949
 
3817
3950
  self._client.trace.finish_turn(
3818
- 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,
3819
3954
  )
3820
3955
 
3821
3956
  # ACP step() boundaries are full remote assistant turns, not
@@ -3991,6 +4126,7 @@ class ACPAgent(AgentBase):
3991
4126
  return
3992
4127
  self._client.trace.abandon()
3993
4128
  self._client.on_event = None
4129
+ self._client.on_text_segment = None
3994
4130
  self._client.on_token = None
3995
4131
  self._client.on_activity = None
3996
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
@@ -0,0 +1,67 @@
1
+ """Expose skills to any ACP agent as files plus a catalog that points at them."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ from collections.abc import Sequence
8
+ from html import escape
9
+ from pathlib import Path
10
+
11
+ from openhands.sdk.skills import Skill
12
+
13
+
14
+ def _description(skill: Skill) -> str:
15
+ if skill.description:
16
+ return skill.description
17
+ return next(
18
+ (line.strip() for line in skill.content.splitlines() if line.strip()),
19
+ skill.name,
20
+ )
21
+
22
+
23
+ def _directory_name(name: str, taken: set[str]) -> str:
24
+ base = re.sub(r"[^A-Za-z0-9._-]+", "-", name).strip(".-") or "skill"
25
+ candidate, suffix = base, 2
26
+ while candidate.lower() in taken:
27
+ candidate, suffix = f"{base}-{suffix}", suffix + 1
28
+ taken.add(candidate.lower())
29
+ return candidate
30
+
31
+
32
+ def write_skill_files(root: Path, skills: Sequence[Skill]) -> dict[str, Path]:
33
+ """Write each skill to ``root/<dir>/SKILL.md``; return its path by skill name."""
34
+ locations: dict[str, Path] = {}
35
+ taken: set[str] = set()
36
+ for skill in skills:
37
+ skill_dir = root / _directory_name(skill.name, taken)
38
+ skill_dir.mkdir(parents=True, exist_ok=True)
39
+ path = skill_dir / "SKILL.md"
40
+ name = json.dumps(skill.name, ensure_ascii=False)
41
+ description = json.dumps(_description(skill)[:1000], ensure_ascii=False)
42
+ path.write_text(
43
+ f"---\nname: {name}\ndescription: {description}\n---\n\n{skill.content}\n",
44
+ encoding="utf-8",
45
+ )
46
+ locations[skill.name] = path
47
+ return locations
48
+
49
+
50
+ def render_skill_catalog(skills: Sequence[Skill], locations: dict[str, Path]) -> str:
51
+ entries = [
52
+ " <skill>\n"
53
+ f" <name>{escape(skill.name)}</name>\n"
54
+ f" <description>{escape(_description(skill)[:1024])}</description>\n"
55
+ f" <location>{escape(str(locations[skill.name]))}</location>\n"
56
+ " </skill>"
57
+ for skill in skills
58
+ if skill.name in locations
59
+ ]
60
+ return (
61
+ "<SKILLS>\n"
62
+ "The following skills are available. When one is relevant to the task, "
63
+ "read its SKILL.md at the given location with your file-reading tool "
64
+ "and follow its instructions.\n\n"
65
+ "<available_skills>\n" + "\n".join(entries) + "\n</available_skills>\n"
66
+ "</SKILLS>"
67
+ )
@@ -51,7 +51,9 @@ 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
56
+ from openhands.sdk.event.types import ROOT_PARENT_ID, EventID
55
57
  from openhands.sdk.llm import (
56
58
  LLM,
57
59
  ImageContent,
@@ -561,12 +563,25 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
561
563
  )
562
564
  return
563
565
 
566
+ # A damaged event log can lose the SystemPromptEvent's file. The first
567
+ # surviving event still names it as its parent, or names the copy that an
568
+ # earlier resume re-created under the same id.
569
+ restored_prompt_id: EventID | None = None
570
+ if has_user_message:
571
+ parent_id = state.events[0].parent_id
572
+ if parent_id is not None and parent_id in state.events:
573
+ parent = state.events[state.events.get_index(parent_id)]
574
+ if isinstance(parent, SystemPromptEvent):
575
+ return
576
+ elif parent_id not in (None, ROOT_PARENT_ID):
577
+ restored_prompt_id = parent_id
578
+
564
579
  # Assert: A user message should never appear before the system prompt.
565
580
  #
566
581
  # NOTE: This is a best-effort check based on the first few events only.
567
582
  # Remote conversations can include a ConversationStateUpdateEvent near the
568
583
  # start, so we scan a small prefix window.
569
- if has_user_message:
584
+ if has_user_message and restored_prompt_id is None:
570
585
  event_types = [type(e).__name__ for e in prefix_events]
571
586
  logger.error(
572
587
  f"init_state: User message found in prefix before SystemPromptEvent! "
@@ -598,7 +613,29 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
598
613
  if dynamic_context
599
614
  else None,
600
615
  )
601
- on_event(event)
616
+ if restored_prompt_id is None:
617
+ on_event(event)
618
+ return
619
+
620
+ # Re-create it as an explicit root under its old id, so the surviving
621
+ # history hangs off it again; appending must not move HEAD onto it.
622
+ logger.warning(
623
+ "SystemPromptEvent '%s' is missing from the event log (its file was "
624
+ "lost); re-creating it as the root of the surviving history. "
625
+ "conversation_id=%s",
626
+ restored_prompt_id,
627
+ state.id,
628
+ )
629
+ head = state.active_branch(limit=1)
630
+ head_is_empty = state.head_is_empty
631
+ on_event(
632
+ event.model_copy(
633
+ update={"id": restored_prompt_id, "parent_id": ROOT_PARENT_ID}
634
+ )
635
+ )
636
+ state.leaf_event_id = head[-1].id if head else None
637
+ state.head_is_empty = head_is_empty
638
+ state.rebuild_view()
602
639
 
603
640
  def get_dynamic_context(self, state: ConversationState) -> str | None:
604
641
  """Get dynamic context for the system prompt, including secrets from state.
@@ -1286,6 +1323,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1286
1323
  thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] | None = None,
1287
1324
  responses_reasoning_item: ReasoningItemModel | None = None,
1288
1325
  stream: StreamContext | None = None,
1326
+ validation_errors: list[ToolValidationError] | None = None,
1289
1327
  ) -> None:
1290
1328
  try:
1291
1329
  json.loads(tool_call.arguments)
@@ -1325,6 +1363,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1325
1363
  tool_name=tool_name,
1326
1364
  tool_call_id=tool_call.id,
1327
1365
  classification=AGENT_OUTCOME,
1366
+ validation_errors=validation_errors,
1328
1367
  )
1329
1368
  record_tool_result(
1330
1369
  conversation,
@@ -1453,6 +1492,18 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1453
1492
  thinking_blocks=thinking_blocks,
1454
1493
  responses_reasoning_item=responses_reasoning_item,
1455
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
+ ),
1456
1507
  )
1457
1508
  return
1458
1509