openhands-sdk 1.26.0__tar.gz → 1.27.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 (271) hide show
  1. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/acp_agent.py +200 -182
  3. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -0
  4. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/utils.py +30 -5
  5. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/local_conversation.py +33 -5
  6. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/remote_conversation.py +2 -2
  7. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/secret_registry.py +36 -1
  8. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/__init__.py +2 -0
  9. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/classifier.py +24 -0
  10. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm.py +77 -0
  11. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/responses_options.py +10 -4
  12. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_info.py +10 -1
  13. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/verified_models.py +2 -0
  14. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/laminar.py +0 -135
  15. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/__init__.py +9 -0
  16. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/acp_providers.py +29 -3
  17. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/api_models.py +15 -3
  18. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/model.py +270 -43
  19. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  20. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/pyproject.toml +1 -1
  21. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/__init__.py +0 -0
  22. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/__init__.py +0 -0
  23. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/acp_models.py +0 -0
  24. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/agent.py +0 -0
  25. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/base.py +0 -0
  26. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  27. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  28. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  29. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  30. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +0 -0
  31. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +0 -0
  32. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +0 -0
  33. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +0 -0
  34. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  35. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  36. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/self_documentation.j2 +0 -0
  37. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  38. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  39. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  40. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  41. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  42. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/banner.py +0 -0
  43. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/__init__.py +0 -0
  44. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/agent_context.py +0 -0
  45. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  46. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/base.py +0 -0
  47. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  48. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  49. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  50. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  51. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/condenser/utils.py +0 -0
  52. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  53. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  54. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  55. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  56. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
  57. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/skills/__init__.py +0 -0
  58. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/__init__.py +0 -0
  59. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  60. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  61. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/base.py +0 -0
  62. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  63. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  64. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  65. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  66. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/context/view/view.py +0 -0
  67. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/__init__.py +0 -0
  68. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/base.py +0 -0
  69. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/cancellation.py +0 -0
  70. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/conversation.py +0 -0
  71. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  72. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/event_store.py +0 -0
  73. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  74. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/exceptions.py +0 -0
  75. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  76. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  77. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  78. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/request.py +0 -0
  79. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  80. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/response_utils.py +0 -0
  81. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/serialization_diff.py +0 -0
  82. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/state.py +0 -0
  83. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  84. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/title_utils.py +0 -0
  85. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/types.py +0 -0
  86. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  87. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  88. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  89. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/__init__.py +0 -0
  90. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/base.py +0 -0
  91. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  92. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  93. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  94. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  95. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  96. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  97. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  98. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  99. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  100. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/critic/result.py +0 -0
  101. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/__init__.py +0 -0
  102. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  103. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/base.py +0 -0
  104. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/condenser.py +0 -0
  105. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/conversation_error.py +0 -0
  106. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/conversation_state.py +0 -0
  107. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/hook_execution.py +0 -0
  108. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  109. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  110. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/action.py +0 -0
  111. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  112. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  113. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  114. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  115. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/resume_transcript.py +0 -0
  116. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/streaming_delta.py +0 -0
  117. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/token.py +0 -0
  118. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/types.py +0 -0
  119. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/event/user_action.py +0 -0
  120. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/__init__.py +0 -0
  121. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/fetch.py +0 -0
  122. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  123. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/info.py +0 -0
  124. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  125. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  126. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  127. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  128. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/cached_repo.py +0 -0
  129. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/exceptions.py +0 -0
  130. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/git_changes.py +0 -0
  131. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/git_diff.py +0 -0
  132. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/models.py +0 -0
  133. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/git/utils.py +0 -0
  134. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/__init__.py +0 -0
  135. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/config.py +0 -0
  136. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/conversation_hooks.py +0 -0
  137. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/executor.py +0 -0
  138. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/manager.py +0 -0
  139. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/hooks/types.py +0 -0
  140. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/__init__.py +0 -0
  141. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/base.py +0 -0
  142. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/cache.py +0 -0
  143. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/local.py +0 -0
  144. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/io/memory.py +0 -0
  145. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/__init__.py +0 -0
  146. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  147. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  148. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/auth/openai.py +0 -0
  149. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  150. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  151. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  152. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
  153. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_registry.py +0 -0
  154. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/llm_response.py +0 -0
  155. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/message.py +0 -0
  156. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  157. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  158. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  159. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/__init__.py +0 -0
  160. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  161. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/options/common.py +0 -0
  162. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/__init__.py +0 -0
  163. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/base.py +0 -0
  164. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  165. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  166. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/streaming.py +0 -0
  167. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  168. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  169. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  170. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  171. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_features.py +0 -0
  172. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  173. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  174. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  175. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  176. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  177. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/__init__.py +0 -0
  178. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/logger.py +0 -0
  179. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/logger/rolling.py +0 -0
  180. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/marketplace/__init__.py +0 -0
  181. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/marketplace/types.py +0 -0
  182. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/__init__.py +0 -0
  183. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/client.py +0 -0
  184. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/definition.py +0 -0
  185. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/exceptions.py +0 -0
  186. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/tool.py +0 -0
  187. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/mcp/utils.py +0 -0
  188. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/__init__.py +0 -0
  189. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/observability/utils.py +0 -0
  190. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/__init__.py +0 -0
  191. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/fetch.py +0 -0
  192. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/installed.py +0 -0
  193. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/loader.py +0 -0
  194. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/plugin.py +0 -0
  195. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/source.py +0 -0
  196. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/plugin/types.py +0 -0
  197. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/py.typed +0 -0
  198. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/secret/__init__.py +0 -0
  199. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/secret/secrets.py +0 -0
  200. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/__init__.py +0 -0
  201. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/analyzer.py +0 -0
  202. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  203. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  204. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  205. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  206. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  207. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/ensemble.py +0 -0
  208. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  209. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  210. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  211. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  212. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/risk.py +0 -0
  213. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/security/shell_parser.py +0 -0
  214. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/settings/metadata.py +0 -0
  215. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/__init__.py +0 -0
  216. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/exceptions.py +0 -0
  217. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/execute.py +0 -0
  218. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/fetch.py +0 -0
  219. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/installed.py +0 -0
  220. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/skill.py +0 -0
  221. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/trigger.py +0 -0
  222. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/types.py +0 -0
  223. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/skills/utils.py +0 -0
  224. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/__init__.py +0 -0
  225. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/load.py +0 -0
  226. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/registry.py +0 -0
  227. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/subagent/schema.py +0 -0
  228. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/testing/__init__.py +0 -0
  229. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/testing/test_llm.py +0 -0
  230. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/__init__.py +0 -0
  231. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  232. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  233. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  234. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  235. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/builtins/think.py +0 -0
  236. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/registry.py +0 -0
  237. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/schema.py +0 -0
  238. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/spec.py +0 -0
  239. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/tool/tool.py +0 -0
  240. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/__init__.py +0 -0
  241. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/async_executor.py +0 -0
  242. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/async_utils.py +0 -0
  243. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/cipher.py +0 -0
  244. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/command.py +0 -0
  245. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/datetime.py +0 -0
  246. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/deprecation.py +0 -0
  247. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/github.py +0 -0
  248. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/json.py +0 -0
  249. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/models.py +0 -0
  250. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/paging.py +0 -0
  251. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/path.py +0 -0
  252. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/pydantic_diff.py +0 -0
  253. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  254. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/redact.py +0 -0
  255. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/truncate.py +0 -0
  256. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/utils/visualize.py +0 -0
  257. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/__init__.py +0 -0
  258. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/base.py +0 -0
  259. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/local.py +0 -0
  260. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/models.py +0 -0
  261. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  262. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  263. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/base.py +0 -0
  264. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  265. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/repo.py +0 -0
  266. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands/sdk/workspace/workspace.py +0 -0
  267. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/SOURCES.txt +0 -0
  268. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  269. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/requires.txt +0 -0
  270. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  271. {openhands_sdk-1.26.0 → openhands_sdk-1.27.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.26.0
3
+ Version: 1.27.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
@@ -23,7 +23,7 @@ import os
23
23
  import threading
24
24
  import time
25
25
  import uuid
26
- from collections.abc import Callable, Generator
26
+ from collections.abc import Awaitable, Callable, Generator
27
27
  from concurrent.futures import Future
28
28
  from pathlib import Path
29
29
  from typing import TYPE_CHECKING, Any, Final, Literal, NamedTuple
@@ -73,7 +73,6 @@ from openhands.sdk.event.conversation_error import ConversationErrorEvent
73
73
  from openhands.sdk.llm import LLM, ImageContent, Message, MessageToolCall, TextContent
74
74
  from openhands.sdk.logger import get_logger
75
75
  from openhands.sdk.observability.laminar import maybe_init_laminar, observe
76
- from openhands.sdk.secret import SecretSource
77
76
  from openhands.sdk.settings.acp_providers import (
78
77
  ACPFileSecretSpec,
79
78
  build_session_model_meta,
@@ -143,15 +142,21 @@ MAX_ACP_CONTENT_CHARS: int = 30_000
143
142
  # Env vars that must be removed from the subprocess environment when a
144
143
  # particular "dominant" env var is present.
145
144
  #
146
- # Rationale: some auth mechanisms are mutually exclusive and their env vars
147
- # conflict. For example, CLAUDE_CONFIG_DIR activates Claude Code's OAuth
148
- # credential-file flow. If ANTHROPIC_API_KEY or ANTHROPIC_BASE_URL are
149
- # also present they redirect requests to a different endpoint (e.g. a proxy)
150
- # that doesn't support OAuth bearer tokens, breaking authentication silently.
151
- # When CLAUDE_CONFIG_DIR is detected we strip the conflicting vars so the
152
- # subprocess can reach api.anthropic.com with its own OAuth token.
145
+ # Rationale: Claude Code's subscription auth uses CLAUDE_CODE_OAUTH_TOKEN, a
146
+ # bearer validated against api.anthropic.com. A co-present ANTHROPIC_API_KEY
147
+ # would take precedence over the token (silently bypassing the subscription),
148
+ # and an ANTHROPIC_BASE_URL would route the bearer to a proxy that rejects it
149
+ # either silently breaks the intended OAuth auth. When the OAuth token is the
150
+ # active credential we strip both so the subprocess authenticates with the
151
+ # token against api.anthropic.com.
152
+ #
153
+ # Keyed on the credential itself (CLAUDE_CODE_OAUTH_TOKEN), NOT on
154
+ # CLAUDE_CONFIG_DIR: the config dir is a *location* lever (data-dir isolation,
155
+ # #1019) that is orthogonal to which credential is active. Keying the strip on
156
+ # it wrongly fired during API-key isolation and missed the conflict when the
157
+ # token arrived via env without isolation (#3588).
153
158
  _ENV_CONFLICT_MAP: dict[str, frozenset[str]] = {
154
- "CLAUDE_CONFIG_DIR": frozenset({"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"}),
159
+ "CLAUDE_CODE_OAUTH_TOKEN": frozenset({"ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL"}),
155
160
  }
156
161
 
157
162
  # Number of trailing characters of an ACP session id retained in log lines
@@ -807,6 +812,11 @@ class _OpenHandsACPBridge:
807
812
  # event stream in cleartext. ``None`` ⇒ no-op (bridge used standalone).
808
813
  self.mask: Callable[[str], str] | None = None
809
814
  self._last_activity_signal: float = float("-inf")
815
+ # Monotonic timestamp of the most recent ``session_update``. Unlike the
816
+ # throttled ``_last_activity_signal``, updated on *every* update so the
817
+ # prompt idle-timeout watchdog sees real progress. Armed per turn via
818
+ # ``arm_activity_clock``.
819
+ self._last_activity_monotonic: float = float("-inf")
810
820
  # Telemetry state from UsageUpdate (persists across turns)
811
821
  self._last_cost: float = 0.0 # last cumulative cost seen
812
822
  self._last_cost_by_session: dict[str, float] = {}
@@ -833,6 +843,26 @@ class _OpenHandsACPBridge:
833
843
  # Note: telemetry state (_last_cost, _context_window, _last_activity_signal,
834
844
  # etc.) is intentionally NOT cleared — it accumulates across turns.
835
845
 
846
+ def arm_activity_clock(self) -> None:
847
+ """Mark "now" as the last activity for the idle-timeout watchdog.
848
+
849
+ Called at the start of each prompt (and each retry) so the idle
850
+ window is measured from the moment the prompt is sent rather than
851
+ from a stale value — a server that legitimately takes a while before
852
+ its first ``session_update`` must not be killed prematurely.
853
+ """
854
+ self._last_activity_monotonic = time.monotonic()
855
+
856
+ def seconds_since_last_activity(self) -> float:
857
+ """Seconds since the last ``session_update`` (or ``arm_activity_clock``).
858
+
859
+ Drives the prompt idle-timeout: any streamed token, thought, tool-call
860
+ start/progress, or usage update from the ACP server resets the clock,
861
+ so a steadily-progressing agent never trips the deadline while a
862
+ genuinely silent (hung) server still does.
863
+ """
864
+ return time.monotonic() - self._last_activity_monotonic
865
+
836
866
  def prepare_usage_sync(self, session_id: str) -> asyncio.Event:
837
867
  """Prepare per-turn UsageUpdate synchronization for a session."""
838
868
  event = asyncio.Event()
@@ -890,6 +920,12 @@ class _OpenHandsACPBridge:
890
920
  ) -> None:
891
921
  logger.debug("ACP session_update: type=%s", type(update).__name__)
892
922
 
923
+ # Any update — token, thought, tool-call start/progress, usage — is
924
+ # progress: reset the idle clock so the prompt's inactivity watchdog
925
+ # keeps a steadily-working agent alive (unthrottled, unlike the
926
+ # heartbeat in ``_maybe_signal_activity``).
927
+ self._last_activity_monotonic = time.monotonic()
928
+
893
929
  # Route fork session updates to the fork accumulator. ask_agent() joins
894
930
  # and returns this text to the caller (a UI/network sink), so mask it
895
931
  # like the main-turn path — a secret echoed in a fork session must not
@@ -1212,8 +1248,13 @@ class ACPAgent(AgentBase):
1212
1248
  acp_prompt_timeout: float = Field(
1213
1249
  default=1800.0,
1214
1250
  description=(
1215
- "Timeout in seconds for a single ACP prompt() call. "
1216
- "Prevents indefinite hangs when the ACP server fails to respond."
1251
+ "Inactivity timeout in seconds for a single ACP prompt() call. "
1252
+ "The deadline resets on every update from the ACP server (token, "
1253
+ "thought, tool-call progress, usage), so a steadily-progressing "
1254
+ "agent runs as long as it keeps making progress; the prompt is "
1255
+ "only aborted after this many seconds with no activity at all. "
1256
+ "Prevents indefinite hangs when the ACP server stops responding "
1257
+ "without killing legitimately long-running work."
1217
1258
  ),
1218
1259
  )
1219
1260
  acp_model: str | None = Field(
@@ -1767,25 +1808,18 @@ class ACPAgent(AgentBase):
1767
1808
  advertisement: their values are written to disk, not injected as env
1768
1809
  vars, so advertising them as available env vars would mislead the agent.
1769
1810
  """
1770
- secret_infos = state.secret_registry.get_secret_infos()
1771
- agent_context = self.agent_context
1811
+ # Advertise from state.secret_registry alone — it now holds
1812
+ # agent_context.secrets too (seeded at conversation init, with their
1813
+ # descriptions), so it is the single source for the <CUSTOM_SECRETS>
1814
+ # block. Reserved file-content secrets are written to disk, not injected
1815
+ # as env vars, so drop them from the advertisement.
1772
1816
  file_secret_names = self._present_file_secret_names(state)
1773
- if file_secret_names:
1774
- secret_infos = [
1775
- info
1776
- for info in secret_infos
1777
- if info.get("name") not in file_secret_names
1778
- ]
1779
- if agent_context is not None and agent_context.secrets:
1780
- agent_context = agent_context.model_copy(
1781
- update={
1782
- "secrets": {
1783
- name: secret
1784
- for name, secret in agent_context.secrets.items()
1785
- if name not in file_secret_names
1786
- }
1787
- }
1788
- )
1817
+ secret_infos = [
1818
+ info
1819
+ for info in state.secret_registry.get_secret_infos()
1820
+ if info.get("name") not in file_secret_names
1821
+ ]
1822
+ agent_context = self.agent_context
1789
1823
  if agent_context is None:
1790
1824
  # No caller-supplied context. Only synthesize an empty one for the
1791
1825
  # renderer if we actually have a registry-secret advertisement to
@@ -1795,53 +1829,29 @@ class ACPAgent(AgentBase):
1795
1829
  # suppress.
1796
1830
  if not secret_infos:
1797
1831
  return None
1798
- return AgentContext(current_datetime=None).to_acp_prompt_context(
1799
- additional_secret_infos=secret_infos
1800
- )
1832
+ agent_context = AgentContext(current_datetime=None)
1833
+ elif agent_context.secrets:
1834
+ # The registry already carries these (and their descriptions), so
1835
+ # clear the agent_context copy to advertise from the registry alone
1836
+ # rather than re-merging a redundant second source.
1837
+ agent_context = agent_context.model_copy(update={"secrets": {}})
1801
1838
  return agent_context.to_acp_prompt_context(additional_secret_infos=secret_infos)
1802
1839
 
1803
- def _read_conversation_secret(
1804
- self, state: ConversationState, name: str
1805
- ) -> str | None:
1806
- """Read a secret value from the canonical channel, then the drain.
1807
-
1808
- Prefers ``state.secret_registry`` — the canonical channel, where
1809
- ``create_request`` lifts ``agent_context.secrets`` on the Python /
1810
- OpenHands-cloud path and where ``StartConversationRequest.secrets``
1811
- land — and falls back to ``agent_context.secrets`` for topologies that
1812
- do not call ``create_request`` (notably canvas-local). See #1022.
1813
- """
1814
- if name in state.secret_registry.secret_sources:
1815
- value = state.secret_registry.get_secret_value(name)
1816
- if value:
1817
- return value
1818
- if self.agent_context and self.agent_context.secrets:
1819
- secret = self.agent_context.secrets.get(name)
1820
- if secret is not None:
1821
- return (
1822
- secret.get_value()
1823
- if isinstance(secret, SecretSource)
1824
- else str(secret)
1825
- )
1826
- return None
1827
-
1828
1840
  def _present_file_secret_names(self, state: ConversationState) -> set[str]:
1829
1841
  """Reserved file-content secret names supplied for this conversation.
1830
1842
 
1831
1843
  A name counts as present if it is configured in
1832
- :attr:`acp_file_secrets` *and* appears in either credential channel
1833
- (``state.secret_registry`` or ``agent_context.secrets``). These names
1834
- are materialised to disk and therefore excluded from the plain env-var
1835
- injection and the ``<CUSTOM_SECRETS>`` advertisement (their values are
1836
- file blobs, not env vars the subprocess can reference by name).
1844
+ :attr:`acp_file_secrets` *and* registered in ``state.secret_registry``
1845
+ (which holds ``agent_context.secrets`` too, seeded at conversation
1846
+ init). These names are materialised to disk and therefore excluded from
1847
+ the plain env-var injection and the ``<CUSTOM_SECRETS>`` advertisement
1848
+ (their values are file blobs, not env vars the subprocess can reference
1849
+ by name).
1837
1850
  """
1838
1851
  configured = {spec.secret_name for spec in self.acp_file_secrets}
1839
1852
  if not configured:
1840
1853
  return set()
1841
- present = set(state.secret_registry.secret_sources)
1842
- if self.agent_context and self.agent_context.secrets:
1843
- present |= set(self.agent_context.secrets)
1844
- return present & configured
1854
+ return set(state.secret_registry.secret_sources) & configured
1845
1855
 
1846
1856
  def _acp_file_secret_dir(self, state: ConversationState, subdir: str) -> Path:
1847
1857
  """Durable per-conversation directory for a credential file.
@@ -1882,14 +1892,12 @@ class ACPAgent(AgentBase):
1882
1892
  highest precedence and is honoured as the materialisation target too), so
1883
1893
  leave it untouched.
1884
1894
 
1885
- Claude carve-out: ``CLAUDE_CONFIG_DIR`` also activates Claude Code's
1886
- OAuth credential-file flow, and :data:`_ENV_CONFLICT_MAP` then strips
1887
- ``ANTHROPIC_API_KEY`` / ``ANTHROPIC_BASE_URL``. Relocating it while an API
1888
- key is the active credential (no OAuth token present) would delete a
1889
- working key + proxy URL and break auth, so skip Claude in that case;
1890
- codex/gemini have no such coupling. (Claude's transcripts are already
1891
- cwd-keyed, so the residual shared state is the mostly-inert global
1892
- config.)
1895
+ Claude note: relocating ``CLAUDE_CONFIG_DIR`` is safe under either auth
1896
+ mode. :data:`_ENV_CONFLICT_MAP` is keyed on the OAuth token
1897
+ (``CLAUDE_CODE_OAUTH_TOKEN``), not on ``CLAUDE_CONFIG_DIR``, so setting
1898
+ the config dir for isolation no longer strips a working
1899
+ ``ANTHROPIC_API_KEY`` — API-key Claude gets the same per-conversation
1900
+ isolation (and pause/resume continuity) as OAuth Claude (#3588).
1893
1901
 
1894
1902
  ``HOME`` (gemini-cli's only lever — it hard-codes ``~/.gemini`` and
1895
1903
  ignores ``XDG``) has a wider blast radius than the surgical
@@ -1900,11 +1908,11 @@ class ACPAgent(AgentBase):
1900
1908
  need a narrower scope can pin ``HOME`` via ``acp_env`` (honoured below)
1901
1909
  or leave isolation off for Gemini.
1902
1910
 
1903
- Ordering contract: this runs *after* the secret_registry / agent_context
1904
- drain and the ``acp_env`` update in :meth:`_start_acp_server`, so the
1905
- credential vars it inspects (``ANTHROPIC_API_KEY`` /
1906
- ``CLAUDE_CODE_OAUTH_TOKEN``) are already hydrated into ``env``. Calling it
1907
- earlier would misread the active credential and wrongly relocate Claude.
1911
+ Ordering: this runs *after* the ``secret_registry`` injection and the
1912
+ ``acp_env`` update in :meth:`_start_acp_server` so an ``acp_env`` pin of
1913
+ the data-dir var is visible and wins. Relocation is now credential-blind
1914
+ (the auth-conflict strip is keyed on ``CLAUDE_CODE_OAUTH_TOKEN``, not on
1915
+ the config dir), so the data-dir var it sets never affects auth.
1908
1916
  """
1909
1917
  provider = detect_acp_provider_by_command(self.acp_command)
1910
1918
  if provider is None or provider.data_dir_env_var is None:
@@ -1912,14 +1920,6 @@ class ACPAgent(AgentBase):
1912
1920
  env_var = provider.data_dir_env_var
1913
1921
  if env_var in self.acp_env:
1914
1922
  return
1915
- # Relies on the ordering contract above: ANTHROPIC_API_KEY /
1916
- # CLAUDE_CODE_OAUTH_TOKEN must already be hydrated into env.
1917
- if (
1918
- env_var == "CLAUDE_CONFIG_DIR"
1919
- and env.get("ANTHROPIC_API_KEY")
1920
- and "CLAUDE_CODE_OAUTH_TOKEN" not in env
1921
- ):
1922
- return
1923
1923
  data_dir = self._acp_file_secret_dir(state, provider.key)
1924
1924
  data_dir.mkdir(mode=0o700, parents=True, exist_ok=True)
1925
1925
  env[env_var] = str(data_dir)
@@ -1929,12 +1929,11 @@ class ACPAgent(AgentBase):
1929
1929
  ) -> None:
1930
1930
  """Seed reserved file-content credentials onto disk and point the CLI at them.
1931
1931
 
1932
- For each spec in :attr:`acp_file_secrets` whose secret is present in
1933
- either credential channel (see :meth:`_read_conversation_secret`), write
1934
- its value to the spec's durable per-conversation directory
1935
- (:meth:`_acp_file_secret_dir`) and set the controlling env var
1936
- (``CODEX_HOME`` / ``GOOGLE_APPLICATION_CREDENTIALS``) unless the caller
1937
- pinned it via ``acp_env``.
1932
+ For each spec in :attr:`acp_file_secrets` whose secret is registered in
1933
+ ``state.secret_registry``, write its value to the spec's durable
1934
+ per-conversation directory (:meth:`_acp_file_secret_dir`) and set the
1935
+ controlling env var (``CODEX_HOME`` / ``GOOGLE_APPLICATION_CREDENTIALS``)
1936
+ unless the caller pinned it via ``acp_env``.
1938
1937
 
1939
1938
  Seed-if-absent: a non-empty existing file is preserved, never clobbered
1940
1939
  — so a token the CLI rewrites on refresh (Codex) survives a recycle, and
@@ -1950,7 +1949,7 @@ class ACPAgent(AgentBase):
1950
1949
  """
1951
1950
  for spec in self.acp_file_secrets:
1952
1951
  name = spec.secret_name
1953
- value = self._read_conversation_secret(state, name)
1952
+ value = state.secret_registry.get_secret_value(name)
1954
1953
  if not value:
1955
1954
  continue
1956
1955
  # Seed where the data-dir env var will actually point: an explicit
@@ -2036,28 +2035,17 @@ class ACPAgent(AgentBase):
2036
2035
  client.mask = state.secret_registry.mask_secrets_in_output
2037
2036
 
2038
2037
  # Build the subprocess environment. Precedence, highest first:
2039
- # acp_env > state.secret_registry > agent_context.secrets
2040
- # > os.environ > default_environment
2038
+ # acp_env > state.secret_registry > os.environ > default_environment
2041
2039
  #
2042
- # Conversation credentials (the registry and the agent_context drain)
2043
- # intentionally OVERRIDE ambient os.environ: an explicit per-conversation
2044
- # / provider secret must win over a same-named variable in the
2045
- # agent-server's own environment (os.environ is the wrong process for a
2046
- # remote server). acp_env (deprecated) stays highest.
2040
+ # Conversation credentials intentionally OVERRIDE ambient os.environ: an
2041
+ # explicit per-conversation / provider secret must win over a same-named
2042
+ # variable in the agent-server's own environment. acp_env (deprecated)
2043
+ # stays highest.
2047
2044
  #
2048
- # Two conversation channels, because an ACP subprocess is a black box we
2049
- # cannot name-scan per command (unlike the regular agent's bash tool), so
2050
- # credentials must be injected upfront:
2051
- # - state.secret_registry: the canonical channel
2052
- # (StartConversationRequest.secrets; also where create_request lifts
2053
- # agent_context.secrets on the Python-caller path / OpenHands cloud).
2054
- # - agent_context.secrets drain: the ONLY channel that delivers
2055
- # agent_context.secrets on paths that do NOT call create_request —
2056
- # notably canvas-local, which builds the request in TypeScript and
2057
- # relies on the server's create_agent() to fold llm.api_key into
2058
- # agent_context.secrets. There is no server-side agent_context.secrets
2059
- # → registry lift, so keep this drain until one exists.
2060
- # On a key collision the registry wins over the drain.
2045
+ # agent_context.secrets are seeded into secret_registry at
2046
+ # LocalConversation.__init__ (lower priority than request.secrets), so
2047
+ # the registry is now the single channel for all secrets including
2048
+ # provider credentials folded in by ACPAgentSettings.create_agent().
2061
2049
  env = default_environment()
2062
2050
  env.update(os.environ)
2063
2051
  if self.acp_env:
@@ -2076,34 +2064,17 @@ class ACPAgent(AgentBase):
2076
2064
  # injected as env vars, so exclude their (large blob) names from the
2077
2065
  # plain env-injection below; materialisation sets only the path env var.
2078
2066
  file_secret_names = self._present_file_secret_names(state)
2079
- # agent_context.secrets drain (lower precedence than the registry).
2080
- # Skip keys a higher tier will set acp_env (applied last) and the
2081
- # registry (applied next) to avoid a wasted SecretSource.get_value()
2082
- # (LookupSecret can make an HTTP request).
2083
- registry_names = set(state.secret_registry.secret_sources)
2084
- if self.agent_context and self.agent_context.secrets:
2085
- for name, secret in self.agent_context.secrets.items():
2086
- if (
2087
- name in self.acp_env
2088
- or name in registry_names
2089
- or name in file_secret_names
2090
- ):
2091
- continue
2092
- value = (
2093
- secret.get_value()
2094
- if isinstance(secret, SecretSource)
2095
- else str(secret)
2096
- )
2097
- if value:
2098
- env[name] = value
2099
- # state.secret_registry overrides the drain and ambient os.environ. Skip
2100
- # keys acp_env will set (avoids a redundant LookupSecret.get_value()).
2101
- for name in state.secret_registry.secret_sources:
2102
- if name in self.acp_env or name in file_secret_names:
2103
- continue
2104
- value = state.secret_registry.get_secret_value(name)
2105
- if value:
2106
- env[name] = value
2067
+ # Inject the whole registry: an ACP CLI is a black box we can't
2068
+ # name-scan per command (unlike the regular agent's bash tool), so
2069
+ # credentials must be delivered upfront. Registry values override
2070
+ # ambient os.environ. Skip keys acp_env will set last (avoids a
2071
+ # redundant LookupSecret.get_value()) and file secrets (materialised to
2072
+ # disk below).
2073
+ env.update(
2074
+ state.secret_registry.get_all_secrets_as_env_vars(
2075
+ exclude=set(self.acp_env) | file_secret_names
2076
+ )
2077
+ )
2107
2078
  # Materialise reserved file-content secrets to disk and point their
2108
2079
  # data-dir env vars (CODEX_HOME / GOOGLE_APPLICATION_CREDENTIALS) at the
2109
2080
  # written files. Done before acp_env so an explicit acp_env override of
@@ -2116,18 +2087,16 @@ class ACPAgent(AgentBase):
2116
2087
 
2117
2088
  # Relocate the CLI's data/config root to a per-conversation directory so
2118
2089
  # sandbox-sharing conversations don't race on a shared HOME (#1019).
2119
- # Ordering is load-bearing this must run AFTER the registry /
2120
- # agent_context drain and the acp_env update above (so the credential
2121
- # vars its Claude carve-out inspects ANTHROPIC_API_KEY /
2122
- # CLAUDE_CODE_OAUTH_TOKEN are already in env, and an acp_env pin wins)
2123
- # and BEFORE the conflict-strip below (so a CLAUDE_CONFIG_DIR it sets is
2124
- # still subject to the strip).
2090
+ # Runs after the registry injection and the acp_env update above so an
2091
+ # acp_env pin of the data-dir var wins. Independent of the strip below
2092
+ # (keyed on the OAuth token, not the data-dir var), so ordering relative
2093
+ # to it no longer matters for correctness.
2125
2094
  if self.acp_isolate_data_dir:
2126
2095
  self._isolate_acp_data_dir(state, env)
2127
2096
 
2128
- # Strip env vars that conflict with an active auth mechanism.
2129
- # E.g. CLAUDE_CONFIG_DIR (OAuth credential file) conflicts with
2130
- # ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL (API-key + proxy auth).
2097
+ # Strip env vars that conflict with an active auth mechanism: an active
2098
+ # CLAUDE_CODE_OAUTH_TOKEN must not coexist with ANTHROPIC_API_KEY (which
2099
+ # takes precedence) or ANTHROPIC_BASE_URL (proxies the bearer). See #3588.
2131
2100
  for dominant, conflicts in _ENV_CONFLICT_MAP.items():
2132
2101
  if dominant in env:
2133
2102
  for conflict in conflicts:
@@ -2438,6 +2407,9 @@ class ACPAgent(AgentBase):
2438
2407
  self._client.on_token = on_token
2439
2408
  self._client.on_event = on_event
2440
2409
  self._client.on_activity = self._on_activity
2410
+ # Start the idle-timeout clock fresh for this attempt so the deadline
2411
+ # is measured from the send (or retry), not from a stale value.
2412
+ self._client.arm_activity_clock()
2441
2413
 
2442
2414
  def _cancel_inflight_tool_calls(self) -> None:
2443
2415
  """Emit a terminal ``failed`` ACPToolCallEvent for every tool call
@@ -2695,27 +2667,71 @@ class ACPAgent(AgentBase):
2695
2667
  )
2696
2668
  return response
2697
2669
 
2670
+ def _idle_timeout_message(self) -> str:
2671
+ return (
2672
+ f"ACP prompt timed out after {self.acp_prompt_timeout:.0f}s "
2673
+ "with no activity from the ACP server"
2674
+ )
2675
+
2676
+ async def _await_with_idle_deadline(
2677
+ self,
2678
+ awaitable: Awaitable[PromptResponse | None],
2679
+ *,
2680
+ cancel_on_exit: bool,
2681
+ ) -> PromptResponse | None:
2682
+ """Await *awaitable*, aborting only after a stretch of inactivity.
2683
+
2684
+ The deadline is an *idle* timeout, not a hard turn deadline: any
2685
+ ``session_update`` from the ACP server (token, thought, tool-call
2686
+ start/progress, usage) resets ``acp_prompt_timeout``, so a steadily-
2687
+ progressing agent runs as long as it keeps making progress while a
2688
+ genuinely silent (hung) server is still cut off after the idle window.
2689
+ This is what keeps long-running ACP commands alive (issue
2690
+ agent-canvas#1245).
2691
+
2692
+ ``asyncio.wait`` (not ``wait_for``) drives the polling so an idle-check
2693
+ slice elapsing never cancels the underlying prompt — only a true idle
2694
+ period raises ``TimeoutError``. ``cancel_on_exit`` controls cleanup:
2695
+ the sync path passes ``True`` to cancel the prompt coroutine it owns;
2696
+ the async path passes ``False`` because the portal task must survive
2697
+ for ``astep``'s ``session/cancel`` + drain handler.
2698
+ """
2699
+ idle_limit = self.acp_prompt_timeout
2700
+ fut = asyncio.ensure_future(awaitable)
2701
+ try:
2702
+ while True:
2703
+ remaining = idle_limit - self._client.seconds_since_last_activity()
2704
+ if remaining <= 0:
2705
+ raise TimeoutError(self._idle_timeout_message())
2706
+ # wait() returns when the prompt finishes or the slice elapses,
2707
+ # leaving fut untouched either way.
2708
+ await asyncio.wait({fut}, timeout=remaining)
2709
+ if fut.done():
2710
+ return fut.result()
2711
+ # Slice elapsed: only give up if the server produced nothing in
2712
+ # the meantime, otherwise the loop re-arms with a fresh window.
2713
+ if self._client.seconds_since_last_activity() >= idle_limit:
2714
+ raise TimeoutError(self._idle_timeout_message())
2715
+ finally:
2716
+ if cancel_on_exit and not fut.done():
2717
+ fut.cancel()
2718
+
2698
2719
  async def _await_prompt_response_with_timeout(
2699
2720
  self,
2700
2721
  prompt_future: Future[PromptResponse | None],
2701
2722
  ) -> PromptResponse | None:
2702
- """Await an ACP prompt with a hard turn deadline.
2723
+ """Await an ACP prompt with an idle (inactivity) turn deadline.
2703
2724
 
2704
- The terminal tool reports hard command timeouts back to the agent
2705
- instead of waiting forever for active commands. ACP prompts follow the
2706
- same rule: activity heartbeats keep the server alive, but they do not
2707
- extend this prompt deadline. The timeout handler sends ``session/cancel``
2708
- and closes any in-flight tool cards.
2725
+ Wraps the portal-side prompt future in :meth:`_await_with_idle_deadline`
2726
+ so the prompt is only abandoned after ``acp_prompt_timeout`` seconds
2727
+ with no ACP activity. The timeout handler in ``astep`` sends
2728
+ ``session/cancel`` and closes any in-flight tool cards.
2709
2729
  """
2710
- try:
2711
- return await asyncio.wait_for(
2712
- asyncio.shield(asyncio.wrap_future(prompt_future)),
2713
- timeout=self.acp_prompt_timeout,
2714
- )
2715
- except TimeoutError as exc:
2716
- raise TimeoutError(
2717
- f"ACP prompt timed out after {self.acp_prompt_timeout:.0f}s"
2718
- ) from exc
2730
+ # cancel_on_exit=False: the portal task behind ``prompt_future`` must
2731
+ # outlive an idle timeout / cancellation so astep's drain can observe it.
2732
+ return await self._await_with_idle_deadline(
2733
+ asyncio.wrap_future(prompt_future), cancel_on_exit=False
2734
+ )
2719
2735
 
2720
2736
  @staticmethod
2721
2737
  def _prompt_response_was_cancelled(response: PromptResponse | None) -> bool:
@@ -2797,12 +2813,11 @@ class ACPAgent(AgentBase):
2797
2813
  state: ConversationState,
2798
2814
  on_event: ConversationCallbackType,
2799
2815
  ) -> None:
2800
- """Error path when ``conn.prompt`` exceeded ``acp_prompt_timeout``."""
2816
+ """Error path when ``conn.prompt`` went idle past ``acp_prompt_timeout``."""
2801
2817
  logger.error(
2802
- "ACP prompt timed out after %.1fs (limit=%.0fs). "
2803
- "The ACP server may have completed its work but failed to "
2804
- "send the JSON-RPC response. Accumulated %d text chunks, "
2805
- "%d tool calls.",
2818
+ "ACP prompt timed out after %.1fs with no activity for the last "
2819
+ "%.0fs. The ACP server may have stalled or failed to send the "
2820
+ "JSON-RPC response. Accumulated %d text chunks, %d tool calls.",
2806
2821
  elapsed,
2807
2822
  self.acp_prompt_timeout,
2808
2823
  len(self._client.accumulated_text),
@@ -2813,9 +2828,10 @@ class ACPAgent(AgentBase):
2813
2828
  content=[
2814
2829
  TextContent(
2815
2830
  text=(
2816
- f"ACP prompt timed out after {elapsed:.0f}s. "
2817
- "The agent may have completed its work but "
2818
- "the response was not received."
2831
+ "ACP prompt timed out after "
2832
+ f"{self.acp_prompt_timeout:.0f}s with no activity from "
2833
+ "the agent. The agent may have stalled, or it may have "
2834
+ "completed its work but the response was not received."
2819
2835
  )
2820
2836
  )
2821
2837
  ],
@@ -2940,7 +2956,7 @@ class ACPAgent(AgentBase):
2940
2956
  t0 = time.monotonic()
2941
2957
  try:
2942
2958
  logger.info(
2943
- "Sending ACP prompt (timeout=%.0fs, blocks=%d)",
2959
+ "Sending ACP prompt (idle_timeout=%.0fs, blocks=%d)",
2944
2960
  self.acp_prompt_timeout,
2945
2961
  len(prompt_blocks),
2946
2962
  )
@@ -2949,14 +2965,16 @@ class ACPAgent(AgentBase):
2949
2965
 
2950
2966
  async def _prompt() -> PromptResponse | None:
2951
2967
  # Thin closure so existing mocks of ``_executor.run_async``
2952
- # that take a single positional callable keep working.
2953
- return await self._do_acp_prompt(prompt_blocks)
2968
+ # that take a single positional callable keep working. The idle
2969
+ # deadline is enforced inside (cancel_on_exit=True: this path
2970
+ # owns the coroutine) rather than as a hard run_async timeout.
2971
+ return await self._await_with_idle_deadline(
2972
+ self._do_acp_prompt(prompt_blocks), cancel_on_exit=True
2973
+ )
2954
2974
 
2955
2975
  for attempt in range(max_retries + 1):
2956
2976
  try:
2957
- response = self._executor.run_async(
2958
- _prompt, timeout=self.acp_prompt_timeout
2959
- )
2977
+ response = self._executor.run_async(_prompt)
2960
2978
  break
2961
2979
  except TimeoutError:
2962
2980
  raise
@@ -3081,7 +3099,7 @@ class ACPAgent(AgentBase):
3081
3099
  prompt_future: Future[PromptResponse | None] | None = None
3082
3100
  try:
3083
3101
  logger.info(
3084
- "Sending ACP prompt (timeout=%.0fs, blocks=%d, async)",
3102
+ "Sending ACP prompt (idle_timeout=%.0fs, blocks=%d, async)",
3085
3103
  self.acp_prompt_timeout,
3086
3104
  len(prompt_blocks),
3087
3105
  )
@@ -31,6 +31,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
31
31
 
32
32
  <CODE_QUALITY>
33
33
  * Write clean, efficient code with minimal comments. Avoid redundancy in comments: Do not repeat information that can be easily inferred from the code itself.
34
+ * Only add a comment when the code expresses something genuinely unintuitive (a non-obvious invariant, a workaround, a subtle ordering/locking requirement, or a deliberate trade-off). Do NOT restate the code, narrate the diff/change history, or describe non-local behavior — that context belongs in the PR description or commit message, not in the source.
34
35
  * When implementing solutions, focus on making the minimal changes needed to solve the problem.
35
36
  * Before implementing any changes, first thoroughly understand the codebase through exploration.
36
37
  * If you are adding a lot of code to a function or file, consider splitting the function or file into smaller pieces when appropriate.