openhands-sdk 1.56.0__tar.gz → 1.58.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/PKG-INFO +1 -1
  2. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/agent.py +7 -11
  3. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/base.py +1 -3
  4. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/base.py +4 -0
  5. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +4 -0
  6. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/base.py +29 -1
  7. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/controller.py +6 -4
  8. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/local_conversation.py +19 -6
  9. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/remote_conversation.py +57 -41
  10. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/request.py +99 -1
  11. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/state.py +8 -8
  12. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/title_utils.py +29 -12
  13. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/action.py +30 -0
  14. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/resume_transcript.py +1 -1
  15. openhands_sdk-1.58.0/openhands/sdk/git/cached_repo.py +1181 -0
  16. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_diff.py +43 -5
  17. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/utils.py +2 -2
  18. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/conversation_hooks.py +43 -1
  19. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/meta_profile_store.py +6 -1
  20. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/verified_models.py +3 -1
  21. openhands_sdk-1.58.0/openhands/sdk/logger/_rich_handler.py +62 -0
  22. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/logger.py +3 -2
  23. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registry.py +24 -0
  24. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/types.py +8 -3
  25. openhands_sdk-1.58.0/openhands/sdk/mcp/oauth.py +203 -0
  26. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/utils.py +7 -2
  27. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile_store.py +16 -1
  28. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/metadata.py +2 -0
  29. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/model.py +140 -0
  30. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/skill.py +6 -8
  31. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/utils.py +10 -11
  32. openhands_sdk-1.58.0/openhands/sdk/utils/cipher.py +131 -0
  33. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/command.py +11 -10
  34. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/models.py +14 -1
  35. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/pydantic_secrets.py +49 -5
  36. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/base.py +2 -2
  37. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/repo.py +52 -11
  38. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/PKG-INFO +1 -1
  39. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/SOURCES.txt +2 -0
  40. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/pyproject.toml +1 -1
  41. openhands_sdk-1.56.0/openhands/sdk/git/cached_repo.py +0 -517
  42. openhands_sdk-1.56.0/openhands/sdk/mcp/oauth.py +0 -85
  43. openhands_sdk-1.56.0/openhands/sdk/utils/cipher.py +0 -81
  44. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/__init__.py +0 -0
  45. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/__init__.py +0 -0
  46. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_agent.py +0 -0
  47. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_contracts.py +0 -0
  48. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_file_credentials.py +0 -0
  49. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_model_discovery.py +0 -0
  50. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_models.py +0 -0
  51. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/acp_tracing.py +0 -0
  52. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/critic_mixin.py +0 -0
  53. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/parallel_executor.py +0 -0
  54. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/response_dispatch.py +0 -0
  55. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/stream_context.py +0 -0
  56. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/agent/utils.py +0 -0
  57. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/automation/__init__.py +0 -0
  58. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/banner.py +0 -0
  59. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/__init__.py +0 -0
  60. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/agent_context.py +0 -0
  61. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/__init__.py +0 -0
  62. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  63. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  64. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_events.j2 +0 -0
  65. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/prompts/summarizing_system.j2 +0 -0
  66. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/condenser/utils.py +0 -0
  67. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/memory.py +0 -0
  68. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/__init__.py +0 -0
  69. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/presets.py +0 -0
  70. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/prompt.py +0 -0
  71. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/registry.py +0 -0
  72. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/section.py +0 -0
  73. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/__init__.py +0 -0
  74. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/dynamic.py +0 -0
  75. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/planning.py +0 -0
  76. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/sections/static.py +0 -0
  77. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  78. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  79. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/__init__.py +0 -0
  80. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/manipulation_indices.py +0 -0
  81. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/__init__.py +0 -0
  82. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/base.py +0 -0
  83. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/batch_atomicity.py +0 -0
  84. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/observation_uniqueness.py +0 -0
  85. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_call_matching.py +0 -0
  86. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/properties/tool_loop_atomicity.py +0 -0
  87. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/context/view/view.py +0 -0
  88. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/__init__.py +0 -0
  89. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/cancellation.py +0 -0
  90. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation.py +0 -0
  91. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/conversation_stats.py +0 -0
  92. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/event_store.py +0 -0
  93. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/events_list_base.py +0 -0
  94. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/exceptions.py +0 -0
  95. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/fifo_lock.py +0 -0
  96. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/__init__.py +0 -0
  97. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/judge.py +0 -0
  98. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/prompts.py +0 -0
  99. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/goal/runner.py +0 -0
  100. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/impl/__init__.py +0 -0
  101. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/message_request.py +0 -0
  102. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/persistence_const.py +0 -0
  103. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/resource_lock_manager.py +0 -0
  104. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/response_utils.py +0 -0
  105. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/secret_registry.py +0 -0
  106. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/stuck_detector.py +0 -0
  107. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/types.py +0 -0
  108. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  109. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/base.py +0 -0
  110. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/conversation/visualizer/default.py +0 -0
  111. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/credential.py +0 -0
  112. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/__init__.py +0 -0
  113. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/base.py +0 -0
  114. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/__init__.py +0 -0
  115. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  116. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/__init__.py +0 -0
  117. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/chat_template.py +0 -0
  118. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/client.py +0 -0
  119. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/critic.py +0 -0
  120. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/api/taxonomy.py +0 -0
  121. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  122. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  123. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/critic/result.py +0 -0
  124. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/__init__.py +0 -0
  125. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/acp_tool_call.py +0 -0
  126. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/base.py +0 -0
  127. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/condenser.py +0 -0
  128. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_error.py +0 -0
  129. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/conversation_state.py +0 -0
  130. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/error_classification.py +0 -0
  131. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/hook_execution.py +0 -0
  132. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_completion_log.py +0 -0
  133. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  134. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/message.py +0 -0
  135. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  136. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/reasoning_utils.py +0 -0
  137. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/llm_convertible/system.py +0 -0
  138. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/security.py +0 -0
  139. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/streaming_delta.py +0 -0
  140. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/token.py +0 -0
  141. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/types.py +0 -0
  142. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/event/user_action.py +0 -0
  143. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/__init__.py +0 -0
  144. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/fetch.py +0 -0
  145. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/__init__.py +0 -0
  146. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/info.py +0 -0
  147. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/interface.py +0 -0
  148. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/manager.py +0 -0
  149. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/metadata.py +0 -0
  150. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/extensions/installation/utils.py +0 -0
  151. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/exceptions.py +0 -0
  152. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_changes.py +0 -0
  153. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/git_commits.py +0 -0
  154. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/git/models.py +0 -0
  155. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/__init__.py +0 -0
  156. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/config.py +0 -0
  157. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/executor.py +0 -0
  158. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/manager.py +0 -0
  159. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/hooks/types.py +0 -0
  160. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/io/__init__.py +0 -0
  161. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/io/base.py +0 -0
  162. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/io/cache.py +0 -0
  163. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/io/local.py +0 -0
  164. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/io/memory.py +0 -0
  165. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/__init__.py +0 -0
  166. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/errors.py +0 -0
  167. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/finalize.py +0 -0
  168. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/preview.py +0 -0
  169. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/launch/resolve.py +0 -0
  170. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/__init__.py +0 -0
  171. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_message_normalization.py +0 -0
  172. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_response_stream.py +0 -0
  173. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/_tokenizer.py +0 -0
  174. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/__init__.py +0 -0
  175. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/credentials.py +0 -0
  176. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/auth/openai.py +0 -0
  177. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/call_context.py +0 -0
  178. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/cleanup_profile.py +0 -0
  179. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  180. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  181. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  182. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/exceptions/types.py +0 -0
  183. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/fallback_strategy.py +0 -0
  184. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm.py +0 -0
  185. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_profile_store.py +0 -0
  186. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_registry.py +0 -0
  187. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/llm_response.py +0 -0
  188. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/message.py +0 -0
  189. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  190. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/fn_call_examples.py +0 -0
  191. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  192. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/__init__.py +0 -0
  193. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/chat_options.py +0 -0
  194. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/common.py +0 -0
  195. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/options/responses_options.py +0 -0
  196. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/provider_connection_store.py +0 -0
  197. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/__init__.py +0 -0
  198. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/base.py +0 -0
  199. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  200. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/router/impl/random.py +0 -0
  201. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/streaming.py +0 -0
  202. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_inline.py +0 -0
  203. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/image_resize.py +0 -0
  204. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/litellm_provider.py +0 -0
  205. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/metrics.py +0 -0
  206. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_features.py +0 -0
  207. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_info.py +0 -0
  208. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/model_prompt_spec.py +0 -0
  209. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/openhands_provider.py +0 -0
  210. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/__init__.py +0 -0
  211. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/providers/openrouter.py +0 -0
  212. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/responses_serialization.py +0 -0
  213. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  214. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/runtime_metadata.py +0 -0
  215. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/telemetry.py +0 -0
  216. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  217. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/llm/utils/vertex_preflight.py +0 -0
  218. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/__init__.py +0 -0
  219. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/logger/rolling.py +0 -0
  220. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/__init__.py +0 -0
  221. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/marketplace/registration.py +0 -0
  222. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/__init__.py +0 -0
  223. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/client.py +0 -0
  224. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/config.py +0 -0
  225. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/definition.py +0 -0
  226. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/exceptions.py +0 -0
  227. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/mcp/tool.py +0 -0
  228. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/__init__.py +0 -0
  229. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/laminar.py +0 -0
  230. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/observability/utils.py +0 -0
  231. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/__init__.py +0 -0
  232. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/discovery.py +0 -0
  233. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/fetch.py +0 -0
  234. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/__init__.py +0 -0
  235. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins.py +0 -0
  236. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/agent_plugins_mcp.py +0 -0
  237. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/base.py +0 -0
  238. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/claude_code.py +0 -0
  239. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/mcp-1.0.0.schema.json +0 -0
  240. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/format/schemas/plugin-1.0.0.schema.json +0 -0
  241. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/installed.py +0 -0
  242. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/loader.py +0 -0
  243. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/paths.py +0 -0
  244. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/plugin.py +0 -0
  245. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/source.py +0 -0
  246. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/strict_fetch.py +0 -0
  247. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/plugin/types.py +0 -0
  248. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/__init__.py +0 -0
  249. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/agent_profile.py +0 -0
  250. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/profile_refs.py +0 -0
  251. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/resolver.py +0 -0
  252. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/profiles/seed.py +0 -0
  253. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/py.typed +0 -0
  254. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/__init__.py +0 -0
  255. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/secret/secrets.py +0 -0
  256. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/__init__.py +0 -0
  257. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/_shell_ast.py +0 -0
  258. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/analyzer.py +0 -0
  259. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/confirmation_policy.py +0 -0
  260. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/__init__.py +0 -0
  261. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/pattern.py +0 -0
  262. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/policy_rails.py +0 -0
  263. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/shell_semantics.py +0 -0
  264. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/defense_in_depth/utils.py +0 -0
  265. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/ensemble.py +0 -0
  266. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/__init__.py +0 -0
  267. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/analyzer.py +0 -0
  268. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/grayswan/utils.py +0 -0
  269. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/llm_analyzer.py +0 -0
  270. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/risk.py +0 -0
  271. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/shell_parser.py +0 -0
  272. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_helpers.py +0 -0
  273. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/security/toolshield_llm_analyzer.py +0 -0
  274. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/__init__.py +0 -0
  275. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_install_catalog.py +0 -0
  276. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/acp_providers.py +0 -0
  277. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/settings/api_models.py +0 -0
  278. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/__init__.py +0 -0
  279. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/exceptions.py +0 -0
  280. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/execute.py +0 -0
  281. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/fetch.py +0 -0
  282. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/installed.py +0 -0
  283. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/trigger.py +0 -0
  284. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/skills/types.py +0 -0
  285. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/__init__.py +0 -0
  286. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/load.py +0 -0
  287. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/registry.py +0 -0
  288. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/schema.py +0 -0
  289. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/subagent/scope.py +0 -0
  290. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/__init__.py +0 -0
  291. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/testing/test_llm.py +0 -0
  292. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/__init__.py +0 -0
  293. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/__init__.py +0 -0
  294. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/classify_and_switch_llm.py +0 -0
  295. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/finish.py +0 -0
  296. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/invoke_skill.py +0 -0
  297. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/switch_llm.py +0 -0
  298. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/think.py +0 -0
  299. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/builtins/vision_inspect.py +0 -0
  300. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/client_tool.py +0 -0
  301. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/defaults.py +0 -0
  302. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/registry.py +0 -0
  303. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/schema.py +0 -0
  304. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/spec.py +0 -0
  305. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/tool/tool.py +0 -0
  306. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/__init__.py +0 -0
  307. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_executor.py +0 -0
  308. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/async_utils.py +0 -0
  309. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/datetime.py +0 -0
  310. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/deprecation.py +0 -0
  311. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/files.py +0 -0
  312. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/github.py +0 -0
  313. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/json.py +0 -0
  314. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/masking.py +0 -0
  315. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/paging.py +0 -0
  316. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/path.py +0 -0
  317. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/redact.py +0 -0
  318. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/truncate.py +0 -0
  319. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/utils/visualize.py +0 -0
  320. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/__init__.py +0 -0
  321. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/base.py +0 -0
  322. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/local.py +0 -0
  323. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/models.py +0 -0
  324. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/__init__.py +0 -0
  325. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/async_remote_workspace.py +0 -0
  326. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  327. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands/sdk/workspace/workspace.py +0 -0
  328. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  329. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/requires.txt +0 -0
  330. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/openhands_sdk.egg-info/top_level.txt +0 -0
  331. {openhands_sdk-1.56.0 → openhands_sdk-1.58.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.56.0
3
+ Version: 1.58.0
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
5
  Project-URL: Source, https://github.com/OpenHands/software-agent-sdk
6
6
  Project-URL: Homepage, https://github.com/OpenHands/software-agent-sdk
@@ -625,6 +625,10 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
625
625
  preset = self._prompt_preset or PromptPreset.DEFAULT
626
626
  return create_registry(preset).build(ctx).dynamic
627
627
 
628
+ @staticmethod
629
+ def _mark_conversation_finished(state: ConversationState) -> None:
630
+ state.execution_status = ConversationExecutionStatus.FINISHED
631
+
628
632
  def _execute_actions(
629
633
  self,
630
634
  conversation: LocalConversation,
@@ -648,11 +652,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
648
652
  check_iterative_refinement=lambda ae: (
649
653
  self._check_iterative_refinement(conversation, ae)
650
654
  ),
651
- mark_finished=lambda: setattr(
652
- state,
653
- "execution_status",
654
- ConversationExecutionStatus.FINISHED,
655
- ),
655
+ mark_finished=lambda: self._mark_conversation_finished(state),
656
656
  )
657
657
 
658
658
  async def _aexecute_actions(
@@ -683,11 +683,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
683
683
  check_iterative_refinement=lambda ae: (
684
684
  self._check_iterative_refinement(conversation, ae)
685
685
  ),
686
- mark_finished=lambda: setattr(
687
- state,
688
- "execution_status",
689
- ConversationExecutionStatus.FINISHED,
690
- ),
686
+ mark_finished=lambda: self._mark_conversation_finished(state),
691
687
  )
692
688
 
693
689
  @observe(name="agent.step", ignore_inputs=["state", "on_event"])
@@ -1594,7 +1590,7 @@ class Agent(CriticMixin, ResponseDispatchMixin, AgentBase):
1594
1590
  condenser_config = self.condenser.model_dump(
1595
1591
  exclude={"llm"}, exclude_none=True
1596
1592
  )
1597
- condenser_llm_obj = getattr(self.condenser, "llm", None)
1593
+ condenser_llm_obj = self.condenser.get_llm()
1598
1594
  condenser_llm = (
1599
1595
  condenser_llm_obj.model if condenser_llm_obj is not None else "N/A"
1600
1596
  )
@@ -404,9 +404,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
404
404
  "model_family" not in template_kwargs
405
405
  or "model_variant" not in template_kwargs
406
406
  ):
407
- spec = get_model_prompt_spec(
408
- self.llm.model, getattr(self.llm, "model_canonical_name", None)
409
- )
407
+ spec = get_model_prompt_spec(self.llm.model, self.llm.model_canonical_name)
410
408
  if "model_family" not in template_kwargs and spec.family:
411
409
  template_kwargs["model_family"] = spec.family
412
410
  if "model_variant" not in template_kwargs and spec.variant:
@@ -29,6 +29,10 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
29
29
  step the condenser will use that condensation event to produce a new `View`.
30
30
  """
31
31
 
32
+ def get_llm(self) -> LLM | None:
33
+ """The LLM used by the condenser, if any."""
34
+ return None
35
+
32
36
  @abstractmethod
33
37
  def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
34
38
  """Condense a sequence of events into a potentially smaller list.
@@ -112,6 +112,10 @@ class LLMSummarizingCondenser(RollingCondenser):
112
112
  def handles_condensation_requests(self) -> bool:
113
113
  return True
114
114
 
115
+ def get_llm(self) -> LLM | None:
116
+ """The LLM used by the condenser, if any."""
117
+ return self.llm
118
+
115
119
  def _effective_max_tokens(self, agent_llm: LLM | None) -> int | None:
116
120
  """Return the effective token cap that triggers token-based condensation.
117
121
 
@@ -2,7 +2,7 @@ import contextlib
2
2
  from abc import ABC, abstractmethod
3
3
  from collections.abc import AsyncIterator, Iterable, Mapping
4
4
  from pathlib import Path
5
- from typing import TYPE_CHECKING, Protocol, TypeVar, cast
5
+ from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast
6
6
 
7
7
  from openhands.sdk.conversation.conversation_stats import ConversationStats
8
8
  from openhands.sdk.conversation.events_list_base import EventsListBase
@@ -45,6 +45,7 @@ def _conversation_tag_attributes(
45
45
 
46
46
  if TYPE_CHECKING:
47
47
  from openhands.sdk.agent.base import AgentBase
48
+ from openhands.sdk.conversation.secret_registry import SecretRegistry
48
49
  from openhands.sdk.conversation.state import ConversationExecutionStatus
49
50
  from openhands.sdk.hooks import HookConfig
50
51
 
@@ -122,6 +123,11 @@ class ConversationStateProtocol(Protocol):
122
123
  """The hook configuration for this conversation."""
123
124
  ...
124
125
 
126
+ @property
127
+ def secret_registry(self) -> "SecretRegistry":
128
+ """The secret registry for this conversation."""
129
+ ...
130
+
125
131
 
126
132
  class BaseConversation(ABC):
127
133
  """Abstract base class for conversation implementations.
@@ -131,9 +137,16 @@ class BaseConversation(ABC):
131
137
  exchange, execution control, and state management.
132
138
  """
133
139
 
140
+ _cleanup_initiated: bool = False
141
+ _cleanup_complete: bool = False
142
+ _hook_processor: Any = None
143
+
134
144
  def __init__(self) -> None:
135
145
  """Initialize the base conversation with span tracking."""
136
146
  self._span_ended = False
147
+ self._cleanup_initiated = False
148
+ self._cleanup_complete = False
149
+ self._hook_processor = None
137
150
  # Owned root span. The ``observe`` decorator looks up this attribute
138
151
  # (by name ``_observability_root_span``) on ``self`` at every entry
139
152
  # point and re-attaches it via ``Laminar.use_span`` so that nested
@@ -142,6 +155,21 @@ class BaseConversation(ABC):
142
155
  # that constructed the conversation.
143
156
  self._observability_root_span: RootSpan | None = None
144
157
 
158
+ @property
159
+ def cleanup_initiated(self) -> bool:
160
+ """Whether conversation cleanup has started."""
161
+ return self._cleanup_initiated
162
+
163
+ @property
164
+ def cleanup_complete(self) -> bool:
165
+ """Whether conversation cleanup has finished."""
166
+ return self._cleanup_complete
167
+
168
+ @property
169
+ def hook_processor(self) -> Any:
170
+ """The active hook processor for the conversation, if configured."""
171
+ return self._hook_processor
172
+
145
173
  def _start_observability_span(
146
174
  self,
147
175
  session_id: str,
@@ -103,12 +103,14 @@ class GoalController:
103
103
  def on_run_finished(self, events: Sequence[Event]) -> GoalStep:
104
104
  """Judge the objective after a run and decide whether to continue.
105
105
 
106
- Increments the iteration count, audits ``events`` with the judge LLM,
107
- and returns a :class:`GoalContinue` (with a followup) or a terminal
108
- :class:`GoalDone` (with a :class:`GoalOutcome`).
106
+ Audits ``events`` with the judge LLM, counts the round once the judge
107
+ has returned a verdict, and returns a :class:`GoalContinue` (with a
108
+ followup) or a terminal :class:`GoalDone` (with a :class:`GoalOutcome`).
109
+ A judge call that raises leaves the count unchanged: a failed or
110
+ abandoned audit is not a completed round.
109
111
  """
110
- self.iteration += 1
111
112
  verdict = judge_goal(self.judge_llm, self.objective, events)
113
+ self.iteration += 1
112
114
  logger.info(
113
115
  "Goal audit %d/%d: score=%.2f complete=%s",
114
116
  self.iteration,
@@ -446,8 +446,14 @@ class LocalConversation(BaseConversation):
446
446
  # Single chokepoint: stamps parent_id (catching any event a hook
447
447
  # swapped in downstream of _tree_stamping) and advances HEAD.
448
448
  self._state.append_event(e)
449
+
450
+ def _track_last_user_message(e):
449
451
  # Track user MessageEvent IDs here so hook callbacks (which may
450
452
  # synthesize or alter user messages) are captured in one place.
453
+ # This runs after the caller-supplied callbacks: setting the field
454
+ # can emit a ConversationStateUpdateEvent through on_state_change
455
+ # (the agent server wires it to this chain), and that update must
456
+ # reach subscribers after the message that caused it.
451
457
  if isinstance(e, MessageEvent) and e.source == "user":
452
458
  # Track the latest real user message ID for hook-blocked checks.
453
459
  # Stop-hook feedback is emitted with source="environment".
@@ -461,7 +467,7 @@ class LocalConversation(BaseConversation):
461
467
  # list never run. The visualizer is prepended below and so still renders
462
468
  # ahead of persist — that is local terminal output, not an announcement
463
469
  # a client can act on.
464
- composed_list = [_default_callback] + callback_list
470
+ composed_list = [_default_callback] + callback_list + [_track_last_user_message]
465
471
  # Handle visualization configuration
466
472
  if isinstance(visualizer, ConversationVisualizerBase):
467
473
  # Use custom visualizer instance
@@ -538,8 +544,8 @@ class LocalConversation(BaseConversation):
538
544
  # Idempotent with the create_request() lift; lower priority than
539
545
  # request.secrets (below). On resume, fill-if-absent so a persisted
540
546
  # value is never downgraded (or lost to redacted/no-cipher serialization).
541
- if (ctx := getattr(self.agent, "agent_context", None)) is not None:
542
- ctx_secrets = getattr(ctx, "secrets", None)
547
+ if self.agent is not None and self.agent.agent_context is not None:
548
+ ctx_secrets = self.agent.agent_context.secrets
543
549
  if ctx_secrets:
544
550
  existing_sources = self._state.secret_registry.secret_sources
545
551
  fill_secrets: dict[str, SecretValue] = {}
@@ -1605,6 +1611,13 @@ class LocalConversation(BaseConversation):
1605
1611
  self._state,
1606
1612
  on_event=self._on_event,
1607
1613
  )
1614
+
1615
+ # Deliver any SessionStart additional_context now that
1616
+ # init_state() has emitted the SystemPromptEvent, so the system
1617
+ # prompt stays first in the LLM request. The hook processor
1618
+ # collected it earlier during _ensure_plugins_loaded().
1619
+ if self._hook_processor is not None:
1620
+ self._hook_processor.inject_pending_additional_context()
1608
1621
  except Exception:
1609
1622
  self._close_runtime_tools(runtime_mcp_tools)
1610
1623
  raise
@@ -2846,9 +2859,9 @@ class LocalConversation(BaseConversation):
2846
2859
 
2847
2860
  def close(self) -> None:
2848
2861
  """Close the conversation and clean up all tool executors."""
2849
- if getattr(self, "_cleanup_complete", False):
2862
+ if self._cleanup_complete:
2850
2863
  return
2851
- first_attempt = not getattr(self, "_cleanup_initiated", False)
2864
+ first_attempt = not self._cleanup_initiated
2852
2865
  if first_attempt:
2853
2866
  self._cleanup_initiated = True
2854
2867
 
@@ -2863,7 +2876,7 @@ class LocalConversation(BaseConversation):
2863
2876
  logger.debug(f"Could not register accumulated cost: {e}")
2864
2877
 
2865
2878
  logger.debug("Closing conversation and cleaning up tool executors")
2866
- hook_processor = getattr(self, "_hook_processor", None)
2879
+ hook_processor = self._hook_processor
2867
2880
  if hook_processor is not None:
2868
2881
  hook_processor.run_session_end()
2869
2882
  try:
@@ -29,7 +29,10 @@ from openhands.sdk.conversation.exceptions import (
29
29
  ConversationRunError,
30
30
  WebSocketConnectionError,
31
31
  )
32
- from openhands.sdk.conversation.secret_registry import SecretValue
32
+ from openhands.sdk.conversation.secret_registry import (
33
+ SecretRegistry,
34
+ SecretValue,
35
+ )
33
36
  from openhands.sdk.conversation.state import ConversationExecutionStatus
34
37
  from openhands.sdk.conversation.title_utils import generate_conversation_title
35
38
  from openhands.sdk.conversation.types import (
@@ -719,6 +722,15 @@ class RemoteState(ConversationStateProtocol):
719
722
  return HookConfig.model_validate(hook_config_data)
720
723
  return None
721
724
 
725
+ @property
726
+ def secret_registry(self) -> SecretRegistry:
727
+ """The secret registry for this conversation."""
728
+ info = self._get_conversation_info()
729
+ secret_registry_data = info.get("secret_registry")
730
+ if secret_registry_data is not None:
731
+ return SecretRegistry.model_validate(secret_registry_data)
732
+ return SecretRegistry()
733
+
722
734
  def model_dump(self, **_kwargs):
723
735
  """Get a dictionary representation of the remote state."""
724
736
  info = self._get_conversation_info()
@@ -817,6 +829,7 @@ class RemoteConversation(BaseConversation):
817
829
  token_callbacks: Optional callbacks invoked with each streamed token
818
830
  delta the server sends (agent LLM with ``stream=True``).
819
831
  """
832
+ super().__init__()
820
833
  observability_metadata = merge_observability_metadata(observability_metadata)
821
834
  observability_parent_span_context = (
822
835
  observability_parent_span_context
@@ -1917,52 +1930,55 @@ class RemoteConversation(BaseConversation):
1917
1930
  The workspace owns the client and will close it during its own cleanup.
1918
1931
  Closing it here would prevent the workspace from making cleanup API calls.
1919
1932
  """
1920
- if self._cleanup_initiated:
1933
+ if self._cleanup_complete:
1921
1934
  return
1922
- self._cleanup_initiated = True
1923
-
1924
- # Best-effort: hand the accumulated LLM cost to the workspace so it can
1925
- # be included in the automation completion callback. Only read cached
1926
- # state — close() also runs on the failure path, where a live fetch
1927
- # could block until timeout against an agent server that is already gone.
1928
- # The cache tracks this run: the agent server streams a "stats" update
1929
- # after every LLM response (EventService._setup_stats_streaming), so it
1930
- # holds the run's spend even on the failure path, which wakes run() from
1931
- # a per-field ERROR/STUCK update before the post-run full-state snapshot.
1932
- try:
1933
- cached = self._state._cached_state
1934
- # Require an actual "stats" entry: a cache built only from partial
1935
- # field updates — e.g. the subscribe-time push for a service with no
1936
- # live conversation — would otherwise yield 0.0 and record a run as
1937
- # free when its cost is really just unknown.
1938
- if cached is not None and "stats" in cached:
1939
- cost = self._state.stats.get_combined_metrics().accumulated_cost
1940
- self.workspace.register_cost(cost)
1941
- except Exception as e:
1942
- logger.debug(f"Could not register accumulated cost: {e}")
1943
-
1944
- # SessionEnd hooks are executed server-side (via hook_config in payload).
1945
- try:
1946
- # Stop WebSocket client if it exists
1947
- if self._ws_client:
1948
- self._ws_client.stop()
1949
- self._ws_client = None
1950
- except Exception:
1951
- pass
1935
+ first_attempt = not self._cleanup_initiated
1936
+ if first_attempt:
1937
+ self._cleanup_initiated = True
1938
+
1939
+ # Best-effort: hand the accumulated LLM cost to the workspace so it can
1940
+ # be included in the automation completion callback. Only read cached
1941
+ # state — close() also runs on the failure path, where a live fetch
1942
+ # could block until timeout against an agent server that is already gone.
1943
+ # The cache tracks this run: the agent server streams a "stats" update
1944
+ # after every LLM response (EventService._setup_stats_streaming), so it
1945
+ # holds the run's spend even on the failure path, which wakes run() from
1946
+ # a per-field ERROR/STUCK update before the post-run full-state snapshot.
1947
+ try:
1948
+ cached = self._state._cached_state
1949
+ # Require an actual "stats" entry: a cache built only from partial
1950
+ # field updates — e.g. the subscribe-time push for a service with no
1951
+ # live conversation — would otherwise yield 0.0 and record a run as
1952
+ # free when its cost is really just unknown.
1953
+ if cached is not None and "stats" in cached:
1954
+ cost = self._state.stats.get_combined_metrics().accumulated_cost
1955
+ self.workspace.register_cost(cost)
1956
+ except Exception as e:
1957
+ logger.debug(f"Could not register accumulated cost: {e}")
1952
1958
 
1953
- self._end_observability_span()
1954
- if self.delete_on_close:
1959
+ # SessionEnd hooks are executed server-side (via hook_config in payload).
1955
1960
  try:
1956
- # trigger server-side delete_conversation to release resources
1957
- # like tmux sessions
1958
- _send_request(
1959
- self._client,
1960
- "DELETE",
1961
- f"{CONVERSATIONS_PATH}/{self.id}",
1962
- )
1961
+ # Stop WebSocket client if it exists
1962
+ if self._ws_client:
1963
+ self._ws_client.stop()
1964
+ self._ws_client = None
1963
1965
  except Exception:
1964
1966
  pass
1965
1967
 
1968
+ self._end_observability_span()
1969
+ if self.delete_on_close:
1970
+ try:
1971
+ # trigger server-side delete_conversation to release resources
1972
+ # like tmux sessions
1973
+ _send_request(
1974
+ self._client,
1975
+ "DELETE",
1976
+ f"{CONVERSATIONS_PATH}/{self.id}",
1977
+ )
1978
+ except Exception:
1979
+ pass
1980
+ self._cleanup_complete = True
1981
+
1966
1982
  def __del__(self) -> None:
1967
1983
  try:
1968
1984
  self.close()
@@ -41,7 +41,16 @@ from openhands.sdk.security.confirmation_policy import (
41
41
  ConfirmationPolicyBase,
42
42
  NeverConfirm,
43
43
  )
44
- from openhands.sdk.settings.model import validate_agent_settings
44
+ from openhands.sdk.settings.model import (
45
+ DEFAULT_WORKTREE_BRANCH_PREFIX,
46
+ DOCKER_MEMORY_PATTERN,
47
+ GIT_BRANCH_PREFIX_PATTERN,
48
+ GIT_REF_PATTERN,
49
+ MIN_CONTAINER_CPUS,
50
+ ConversationRuntime,
51
+ WorktreeBase,
52
+ validate_agent_settings,
53
+ )
45
54
  from openhands.sdk.skills import Skill
46
55
  from openhands.sdk.subagent.schema import AgentDefinition
47
56
  from openhands.sdk.tool.client_tool import ClientToolSpec
@@ -151,6 +160,68 @@ class AgentLaunchAdditions(BaseModel):
151
160
  )
152
161
 
153
162
 
163
+ class ConversationContainerOptions(BaseModel):
164
+ """Resources for a conversation's container.
165
+
166
+ Only the Docker runtime applies them, when it creates the conversation's
167
+ container; a local conversation ignores them. An unset field keeps the
168
+ server's configured limit; a set one replaces it and is not capped by it.
169
+ """
170
+
171
+ model_config = ConfigDict(extra="forbid")
172
+
173
+ memory: str | None = Field(
174
+ default=None,
175
+ pattern=DOCKER_MEMORY_PATTERN,
176
+ description=(
177
+ "Container memory limit in `docker run --memory` syntax; replaces "
178
+ "the server's limit and is not capped by it."
179
+ ),
180
+ )
181
+ cpus: float | None = Field(
182
+ default=None,
183
+ ge=MIN_CONTAINER_CPUS,
184
+ description=(
185
+ "Number of CPUs the container may use (`docker run --cpus`); "
186
+ "replaces the server's limit and is not capped by it."
187
+ ),
188
+ )
189
+ browser: bool = Field(
190
+ default=True,
191
+ description=(
192
+ "False turns the browser off. True cannot turn it on for an image "
193
+ "without one."
194
+ ),
195
+ )
196
+
197
+
198
+ class ConversationWorktreeOptions(BaseModel):
199
+ """How the server creates a conversation's worktree (``worktree=True``)."""
200
+
201
+ model_config = ConfigDict(extra="forbid")
202
+
203
+ base: WorktreeBase = Field(
204
+ default="default_branch",
205
+ description="What the worktree branches from; `ref` uses `ref`.",
206
+ )
207
+ ref: str | None = Field(
208
+ default=None,
209
+ pattern=GIT_REF_PATTERN,
210
+ description="Branch, tag or commit to branch from when `base` is `ref`.",
211
+ )
212
+ branch_prefix: str = Field(
213
+ default=DEFAULT_WORKTREE_BRANCH_PREFIX,
214
+ pattern=GIT_BRANCH_PREFIX_PATTERN,
215
+ description="Start of the worktree's branch name; the conversation id ends it.",
216
+ )
217
+
218
+ @model_validator(mode="after")
219
+ def _ref_for_ref_base(self) -> ConversationWorktreeOptions:
220
+ if self.base == "ref" and self.ref is None:
221
+ raise ValueError("A `ref` base needs a `ref`")
222
+ return self
223
+
224
+
154
225
  class ConversationConfig(BaseModel):
155
226
  """Shared conversation configuration — everything except the agent.
156
227
 
@@ -175,6 +246,10 @@ class ConversationConfig(BaseModel):
175
246
  "`/tmp/conversation-worktrees/<conversation_id>/<project_name>`."
176
247
  ),
177
248
  )
249
+ worktree_options: ConversationWorktreeOptions | None = Field(
250
+ default=None,
251
+ description="How the worktree is created (`worktree=True`); unset: defaults.",
252
+ )
178
253
  conversation_id: UUID | None = Field(
179
254
  default=None,
180
255
  description=(
@@ -346,6 +421,17 @@ class ConversationConfig(BaseModel):
346
421
  "the agent's LLM."
347
422
  ),
348
423
  )
424
+ prompt: str | None = Field(
425
+ default=None,
426
+ max_length=2000,
427
+ description=(
428
+ "Optional prompt that replaces the default title-generation user "
429
+ "message. Use {conversation_content} to place the first user "
430
+ "message and {max_length} to place the title length limit. If the "
431
+ "conversation placeholder is omitted, the message is appended "
432
+ "automatically. Empty or unset values use the default prompt."
433
+ ),
434
+ )
349
435
 
350
436
 
351
437
  class StartConversationRequest(ConversationConfig):
@@ -379,6 +465,18 @@ class StartConversationRequest(ConversationConfig):
379
465
  ),
380
466
  )
381
467
  agent: AgentBase = Field(default=cast(AgentBase, None))
468
+ conversation_runtime: ConversationRuntime | None = Field(
469
+ default=None,
470
+ description=(
471
+ "Runtime that executes this conversation. Unset uses the server's "
472
+ "configured `conversation_runtime`; another value is only accepted "
473
+ "when the server lists it in `available_conversation_runtimes`."
474
+ ),
475
+ )
476
+ container: ConversationContainerOptions | None = Field(
477
+ default=None,
478
+ description="Resources for the conversation's container (Docker runtime).",
479
+ )
382
480
 
383
481
  @model_validator(mode="before")
384
482
  @classmethod
@@ -5,7 +5,7 @@ from collections.abc import Callable, Sequence
5
5
  from contextlib import AbstractContextManager
6
6
  from enum import Enum
7
7
  from pathlib import Path
8
- from typing import Any, Self
8
+ from typing import Any, Self, cast
9
9
 
10
10
  from pydantic import Field, PrivateAttr
11
11
 
@@ -230,7 +230,7 @@ class ConversationState(OpenHandsModel):
230
230
  )
231
231
 
232
232
  # ===== Private attrs (NOT Fields) =====
233
- _fs: FileStore = PrivateAttr() # filestore for persistence
233
+ _fs: FileStore = PrivateAttr(default=cast(Any, None)) # filestore for persistence
234
234
  _events: EventLog = PrivateAttr() # now the storage for events
235
235
  # Cached projection of `_events` for the *active branch*, lazily updated on
236
236
  # read. Derived state — never persisted. `_view_branch_leaf` is the resolved
@@ -603,8 +603,8 @@ class ConversationState(OpenHandsModel):
603
603
  super().__setattr__(name, value)
604
604
 
605
605
  is_field = name in self.__class__.model_fields
606
- autosave_enabled = getattr(self, "_autosave_enabled", False)
607
- fs = getattr(self, "_fs", None)
606
+ autosave_enabled = self._autosave_enabled
607
+ fs = self._fs
608
608
 
609
609
  if not (autosave_enabled and is_field and fs is not None):
610
610
  return
@@ -612,7 +612,7 @@ class ConversationState(OpenHandsModel):
612
612
  if old is _sentinel or old != value:
613
613
  # Inside a context-manager block, defer the save until __exit__
614
614
  # so that multiple field mutations produce a single I/O write.
615
- if getattr(self, "_save_depth", 0) > 0:
615
+ if self._save_depth > 0:
616
616
  self._dirty = True
617
617
  else:
618
618
  try:
@@ -625,7 +625,7 @@ class ConversationState(OpenHandsModel):
625
625
  # skipped: they are internal HEAD bookkeeping (leaf_event_id changes
626
626
  # every event, so broadcasting it would ~double the persisted log) and
627
627
  # the HEAD is recoverable from the just-appended event.
628
- callback = getattr(self, "_on_state_change", None)
628
+ callback = self._on_state_change
629
629
  if (
630
630
  callback is not None
631
631
  and old is not _sentinel
@@ -755,8 +755,8 @@ class ConversationState(OpenHandsModel):
755
755
  try:
756
756
  self._save_depth -= 1
757
757
  if self._save_depth == 0 and self._dirty:
758
- fs = getattr(self, "_fs", None)
759
- autosave_enabled = getattr(self, "_autosave_enabled", False)
758
+ fs = self._fs
759
+ autosave_enabled = self._autosave_enabled
760
760
  if autosave_enabled and fs is not None:
761
761
  self._save_base_state(fs)
762
762
  self._dirty = False
@@ -2,6 +2,7 @@
2
2
 
3
3
  import re
4
4
  from collections.abc import Callable, Sequence
5
+ from typing import Final
5
6
 
6
7
  from openhands.sdk.event import MessageEvent
7
8
  from openhands.sdk.event.base import Event
@@ -30,6 +31,9 @@ categories = [
30
31
  {"emoji": "♻️", "name": "refactor", "description": "Code refactoring"},
31
32
  ]
32
33
 
34
+ CONVERSATION_CONTENT_PLACEHOLDER: Final[str] = "{conversation_content}"
35
+ MAX_LENGTH_PLACEHOLDER: Final[str] = "{max_length}"
36
+
33
37
 
34
38
  def extract_message_text(event: MessageEvent) -> str | None:
35
39
  """Extract plain-text content from a message event."""
@@ -83,6 +87,7 @@ def generate_title_with_llm(
83
87
  *,
84
88
  call_context: LLMCallContext | None = None,
85
89
  on_error: Callable[[Exception], None] | None = None,
90
+ prompt: str | None = None,
86
91
  ) -> str | None:
87
92
  """Generate a conversation title using LLM.
88
93
 
@@ -93,6 +98,10 @@ def generate_title_with_llm(
93
98
  on_error: Optional callback invoked with the exception when the LLM
94
99
  call fails. Title generation still falls back (returns None); the
95
100
  callback lets callers surface the otherwise-swallowed error.
101
+ prompt: Optional user-message prompt override. The
102
+ ``{conversation_content}`` and ``{max_length}`` placeholders are
103
+ replaced when present. If the conversation placeholder is omitted,
104
+ the message content is appended automatically.
96
105
 
97
106
  Returns:
98
107
  Generated title, or None if LLM fails or returns empty response.
@@ -107,6 +116,23 @@ def generate_title_with_llm(
107
116
  f"{c['emoji']} {c['name']}: {c['description']}" for c in categories
108
117
  )
109
118
 
119
+ template = (prompt or "").strip()
120
+ if template:
121
+ user_prompt = template.replace(
122
+ CONVERSATION_CONTENT_PLACEHOLDER, truncated_message
123
+ ).replace(MAX_LENGTH_PLACEHOLDER, str(max_length))
124
+ if CONVERSATION_CONTENT_PLACEHOLDER not in template:
125
+ user_prompt = f"{user_prompt}\n\nConversation content:\n{truncated_message}"
126
+ else:
127
+ user_prompt = (
128
+ f"Generate a title (maximum {max_length} characters) "
129
+ f"for a conversation that starts with this message:\n\n"
130
+ f"{truncated_message}."
131
+ "Also make sure to include ONE most relevant emoji at "
132
+ "the start of the title."
133
+ f" Choose the emoji from this list:{emojis_descriptions} "
134
+ )
135
+
110
136
  try:
111
137
  # Create messages for the LLM to generate a title
112
138
  messages = [
@@ -130,18 +156,7 @@ def generate_title_with_llm(
130
156
  ),
131
157
  Message(
132
158
  role="user",
133
- content=[
134
- TextContent(
135
- text=(
136
- f"Generate a title (maximum {max_length} characters) "
137
- f"for a conversation that starts with this message:\n\n"
138
- f"{truncated_message}."
139
- "Also make sure to include ONE most relevant emoji at "
140
- "the start of the title."
141
- f" Choose the emoji from this list:{emojis_descriptions} "
142
- )
143
- )
144
- ],
159
+ content=[TextContent(text=user_prompt)],
145
160
  ),
146
161
  ]
147
162
 
@@ -204,6 +219,7 @@ def generate_title_from_message(
204
219
  *,
205
220
  call_context: LLMCallContext | None = None,
206
221
  on_error: Callable[[Exception], None] | None = None,
222
+ prompt: str | None = None,
207
223
  ) -> str:
208
224
  """Generate a title from an already-extracted user message."""
209
225
  # Skip the ACP sentinel LLM — it has no credentials and cannot be
@@ -218,6 +234,7 @@ def generate_title_from_message(
218
234
  max_length,
219
235
  call_context=call_context,
220
236
  on_error=on_error,
237
+ prompt=prompt,
221
238
  )
222
239
  if llm_title:
223
240
  return llm_title