openhands-sdk 1.3.0__tar.gz → 1.4.1__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 (169) hide show
  1. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/PKG-INFO +2 -2
  2. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/__init__.py +4 -0
  3. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/agent.py +55 -22
  4. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/base.py +8 -1
  5. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/system_prompt.j2 +1 -11
  6. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/utils.py +5 -0
  7. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/agent_context.py +30 -0
  8. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/skills/__init__.py +2 -0
  9. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/skills/skill.py +202 -1
  10. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/__init__.py +5 -1
  11. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/base.py +15 -6
  12. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/conversation.py +10 -1
  13. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/conversation_stats.py +38 -1
  14. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/fifo_lock.py +14 -8
  15. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/impl/local_conversation.py +21 -5
  16. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/secret_source.py +1 -1
  17. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/state.py +8 -0
  18. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/types.py +5 -0
  19. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/conversation_state.py +8 -0
  20. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/__init__.py +3 -0
  21. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/llm.py +82 -16
  22. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/llm_registry.py +1 -1
  23. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/options/chat_options.py +12 -24
  24. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/options/responses_options.py +9 -1
  25. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/router/base.py +3 -0
  26. openhands_sdk-1.4.1/openhands/sdk/llm/streaming.py +9 -0
  27. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/model_features.py +12 -0
  28. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/logger/logger.py +7 -0
  29. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/tool.py +18 -1
  30. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/models.py +90 -9
  31. openhands_sdk-1.4.1/openhands/sdk/utils/truncate.py +117 -0
  32. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/__init__.py +3 -1
  33. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/models.py +7 -1
  34. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/remote/async_remote_workspace.py +22 -1
  35. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/remote/base.py +13 -0
  36. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands_sdk.egg-info/PKG-INFO +2 -2
  37. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands_sdk.egg-info/SOURCES.txt +2 -0
  38. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands_sdk.egg-info/requires.txt +1 -1
  39. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/pyproject.toml +2 -2
  40. openhands_sdk-1.3.0/openhands/sdk/utils/truncate.py +0 -44
  41. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/__init__.py +0 -0
  42. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/in_context_learning_example.j2 +0 -0
  43. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +0 -0
  44. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/security_policy.j2 +0 -0
  45. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/security_risk_assessment.j2 +0 -0
  46. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +0 -0
  47. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +0 -0
  48. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/system_prompt_planning.j2 +0 -0
  49. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +0 -0
  50. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/__init__.py +0 -0
  51. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/__init__.py +0 -0
  52. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/base.py +0 -0
  53. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/llm_summarizing_condenser.py +0 -0
  54. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/no_op_condenser.py +0 -0
  55. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/pipeline_condenser.py +0 -0
  56. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +0 -0
  57. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/prompts/__init__.py +0 -0
  58. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/prompts/prompt.py +0 -0
  59. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/prompts/templates/ask_agent_template.j2 +0 -0
  60. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +0 -0
  61. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +0 -0
  62. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/skills/exceptions.py +0 -0
  63. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/skills/trigger.py +0 -0
  64. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/skills/types.py +0 -0
  65. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/context/view.py +0 -0
  66. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/event_store.py +0 -0
  67. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/events_list_base.py +0 -0
  68. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/exceptions.py +0 -0
  69. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/impl/__init__.py +0 -0
  70. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/impl/remote_conversation.py +0 -0
  71. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/persistence_const.py +0 -0
  72. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/response_utils.py +0 -0
  73. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/secret_registry.py +0 -0
  74. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/serialization_diff.py +0 -0
  75. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/stuck_detector.py +0 -0
  76. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/title_utils.py +0 -0
  77. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/visualizer/__init__.py +0 -0
  78. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/visualizer/base.py +0 -0
  79. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/conversation/visualizer/default.py +0 -0
  80. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/__init__.py +0 -0
  81. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/base.py +0 -0
  82. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/impl/__init__.py +0 -0
  83. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/impl/agent_finished.py +0 -0
  84. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/impl/empty_patch.py +0 -0
  85. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/critic/impl/pass_critic.py +0 -0
  86. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/__init__.py +0 -0
  87. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/base.py +0 -0
  88. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/condenser.py +0 -0
  89. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/conversation_error.py +0 -0
  90. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_completion_log.py +0 -0
  91. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_convertible/__init__.py +0 -0
  92. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_convertible/action.py +0 -0
  93. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_convertible/message.py +0 -0
  94. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_convertible/observation.py +0 -0
  95. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/llm_convertible/system.py +0 -0
  96. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/token.py +0 -0
  97. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/types.py +0 -0
  98. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/event/user_action.py +0 -0
  99. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/git/exceptions.py +0 -0
  100. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/git/git_changes.py +0 -0
  101. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/git/git_diff.py +0 -0
  102. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/git/models.py +0 -0
  103. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/git/utils.py +0 -0
  104. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/io/__init__.py +0 -0
  105. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/io/base.py +0 -0
  106. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/io/local.py +0 -0
  107. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/io/memory.py +0 -0
  108. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/exceptions/__init__.py +0 -0
  109. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/exceptions/classifier.py +0 -0
  110. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/exceptions/mapping.py +0 -0
  111. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/exceptions/types.py +0 -0
  112. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/llm_response.py +0 -0
  113. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/message.py +0 -0
  114. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/mixins/fn_call_converter.py +0 -0
  115. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/mixins/non_native_fc.py +0 -0
  116. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/options/__init__.py +0 -0
  117. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/options/common.py +0 -0
  118. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/router/__init__.py +0 -0
  119. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/router/impl/multimodal.py +0 -0
  120. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/router/impl/random.py +0 -0
  121. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/metrics.py +0 -0
  122. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/model_info.py +0 -0
  123. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/retry_mixin.py +0 -0
  124. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/telemetry.py +0 -0
  125. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/unverified_models.py +0 -0
  126. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/llm/utils/verified_models.py +0 -0
  127. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/logger/__init__.py +0 -0
  128. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/logger/rolling.py +0 -0
  129. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/__init__.py +0 -0
  130. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/client.py +0 -0
  131. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/definition.py +0 -0
  132. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/exceptions.py +0 -0
  133. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/tool.py +0 -0
  134. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/mcp/utils.py +0 -0
  135. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/observability/__init__.py +0 -0
  136. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/observability/laminar.py +0 -0
  137. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/observability/utils.py +0 -0
  138. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/py.typed +0 -0
  139. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/security/__init__.py +0 -0
  140. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/security/analyzer.py +0 -0
  141. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/security/confirmation_policy.py +0 -0
  142. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/security/llm_analyzer.py +0 -0
  143. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/security/risk.py +0 -0
  144. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/__init__.py +0 -0
  145. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/builtins/__init__.py +0 -0
  146. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/builtins/finish.py +0 -0
  147. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/builtins/think.py +0 -0
  148. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/registry.py +0 -0
  149. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/schema.py +0 -0
  150. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/tool/spec.py +0 -0
  151. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/__init__.py +0 -0
  152. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/async_executor.py +0 -0
  153. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/async_utils.py +0 -0
  154. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/cipher.py +0 -0
  155. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/command.py +0 -0
  156. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/deprecation.py +0 -0
  157. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/github.py +0 -0
  158. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/json.py +0 -0
  159. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/pydantic_diff.py +0 -0
  160. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/pydantic_secrets.py +0 -0
  161. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/utils/visualize.py +0 -0
  162. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/base.py +0 -0
  163. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/local.py +0 -0
  164. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/remote/__init__.py +0 -0
  165. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/remote/remote_workspace_mixin.py +0 -0
  166. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands/sdk/workspace/workspace.py +0 -0
  167. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands_sdk.egg-info/dependency_links.txt +0 -0
  168. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/openhands_sdk.egg-info/top_level.txt +0 -0
  169. {openhands_sdk-1.3.0 → openhands_sdk-1.4.1}/setup.cfg +0 -0
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openhands-sdk
3
- Version: 1.3.0
3
+ Version: 1.4.1
4
4
  Summary: OpenHands SDK - Core functionality for building AI agents
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: deprecation>=2.1.0
7
7
  Requires-Dist: fastmcp>=2.11.3
8
8
  Requires-Dist: httpx>=0.27.0
9
- Requires-Dist: litellm>=1.77.7.dev9
9
+ Requires-Dist: litellm>=1.80.7
10
10
  Requires-Dist: pydantic>=2.11.7
11
11
  Requires-Dist: python-frontmatter>=1.1.0
12
12
  Requires-Dist: python-json-logger>=3.3.0
@@ -21,11 +21,13 @@ from openhands.sdk.llm import (
21
21
  LLM,
22
22
  ImageContent,
23
23
  LLMRegistry,
24
+ LLMStreamChunk,
24
25
  Message,
25
26
  RedactedThinkingBlock,
26
27
  RegistryEvent,
27
28
  TextContent,
28
29
  ThinkingBlock,
30
+ TokenCallbackType,
29
31
  )
30
32
  from openhands.sdk.logger import get_logger
31
33
  from openhands.sdk.mcp import (
@@ -58,6 +60,8 @@ except PackageNotFoundError:
58
60
  __all__ = [
59
61
  "LLM",
60
62
  "LLMRegistry",
63
+ "LLMStreamChunk",
64
+ "TokenCallbackType",
61
65
  "ConversationStats",
62
66
  "RegistryEvent",
63
67
  "Message",
@@ -13,6 +13,7 @@ from openhands.sdk.agent.utils import (
13
13
  from openhands.sdk.conversation import (
14
14
  ConversationCallbackType,
15
15
  ConversationState,
16
+ ConversationTokenCallbackType,
16
17
  LocalConversation,
17
18
  )
18
19
  from openhands.sdk.conversation.state import ConversationExecutionStatus
@@ -27,6 +28,7 @@ from openhands.sdk.event import (
27
28
  )
28
29
  from openhands.sdk.event.condenser import Condensation, CondensationRequest
29
30
  from openhands.sdk.llm import (
31
+ LLMResponse,
30
32
  Message,
31
33
  MessageToolCall,
32
34
  ReasoningItemModel,
@@ -135,6 +137,7 @@ class Agent(AgentBase):
135
137
  self,
136
138
  conversation: LocalConversation,
137
139
  on_event: ConversationCallbackType,
140
+ on_token: ConversationTokenCallbackType | None = None,
138
141
  ) -> None:
139
142
  state = conversation.state
140
143
  # Check for pending actions (implicit confirmation)
@@ -167,7 +170,10 @@ class Agent(AgentBase):
167
170
 
168
171
  try:
169
172
  llm_response = make_llm_completion(
170
- self.llm, _messages, tools=list(self.tools_map.values())
173
+ self.llm,
174
+ _messages,
175
+ tools=list(self.tools_map.values()),
176
+ on_token=on_token,
171
177
  )
172
178
  except FunctionCallValidationError as e:
173
179
  logger.warning(f"LLM generated malformed function call: {e}")
@@ -197,6 +203,17 @@ class Agent(AgentBase):
197
203
  # LLMResponse already contains the converted message and metrics snapshot
198
204
  message: Message = llm_response.message
199
205
 
206
+ # Check if this is a reasoning-only response (e.g., from reasoning models)
207
+ # or a message-only response without tool calls
208
+ has_reasoning = (
209
+ message.responses_reasoning_item is not None
210
+ or message.reasoning_content is not None
211
+ or (message.thinking_blocks and len(message.thinking_blocks) > 0)
212
+ )
213
+ has_content = any(
214
+ isinstance(c, TextContent) and c.text.strip() for c in message.content
215
+ )
216
+
200
217
  if message.tool_calls and len(message.tool_calls) > 0:
201
218
  if not all(isinstance(c, TextContent) for c in message.content):
202
219
  logger.warning(
@@ -236,29 +253,30 @@ class Agent(AgentBase):
236
253
  if action_events:
237
254
  self._execute_actions(conversation, action_events, on_event)
238
255
 
239
- else:
256
+ # Emit VLLM token ids if enabled before returning
257
+ self._maybe_emit_vllm_tokens(llm_response, on_event)
258
+ return
259
+
260
+ # No tool calls - emit message event for reasoning or content responses
261
+ if not has_reasoning and not has_content:
262
+ logger.warning("LLM produced empty response - continuing agent loop")
263
+
264
+ msg_event = MessageEvent(
265
+ source="agent",
266
+ llm_message=message,
267
+ llm_response_id=llm_response.id,
268
+ )
269
+ on_event(msg_event)
270
+
271
+ # Emit VLLM token ids if enabled
272
+ self._maybe_emit_vllm_tokens(llm_response, on_event)
273
+
274
+ # Finish conversation if LLM produced content (awaits user input)
275
+ # Continue if only reasoning without content (e.g., GPT-5 codex thinking)
276
+ if has_content:
240
277
  logger.debug("LLM produced a message response - awaits user input")
241
278
  state.execution_status = ConversationExecutionStatus.FINISHED
242
- msg_event = MessageEvent(
243
- source="agent",
244
- llm_message=message,
245
- llm_response_id=llm_response.id,
246
- )
247
- on_event(msg_event)
248
-
249
- # If using VLLM, we can get the raw prompt and response tokens
250
- # that can be useful for RL training.
251
- if (
252
- "return_token_ids" in self.llm.litellm_extra_body
253
- ) and self.llm.litellm_extra_body["return_token_ids"]:
254
- token_event = TokenEvent(
255
- source="agent",
256
- prompt_token_ids=llm_response.raw_response["prompt_token_ids"],
257
- response_token_ids=llm_response.raw_response["choices"][0][
258
- "provider_specific_fields"
259
- ]["token_ids"],
260
- )
261
- on_event(token_event)
279
+ return
262
280
 
263
281
  def _requires_user_confirmation(
264
282
  self, state: ConversationState, action_events: list[ActionEvent]
@@ -483,3 +501,18 @@ class Agent(AgentBase):
483
501
  if tool.name == FinishTool.name:
484
502
  state.execution_status = ConversationExecutionStatus.FINISHED
485
503
  return obs_event
504
+
505
+ def _maybe_emit_vllm_tokens(
506
+ self, llm_response: LLMResponse, on_event: ConversationCallbackType
507
+ ) -> None:
508
+ if (
509
+ "return_token_ids" in self.llm.litellm_extra_body
510
+ ) and self.llm.litellm_extra_body["return_token_ids"]:
511
+ token_event = TokenEvent(
512
+ source="agent",
513
+ prompt_token_ids=llm_response.raw_response["prompt_token_ids"],
514
+ response_token_ids=llm_response.raw_response["choices"][0][
515
+ "provider_specific_fields"
516
+ ]["token_ids"],
517
+ )
518
+ on_event(token_event)
@@ -20,7 +20,10 @@ from openhands.sdk.utils.pydantic_diff import pretty_pydantic_diff
20
20
 
21
21
  if TYPE_CHECKING:
22
22
  from openhands.sdk.conversation import ConversationState, LocalConversation
23
- from openhands.sdk.conversation.types import ConversationCallbackType
23
+ from openhands.sdk.conversation.types import (
24
+ ConversationCallbackType,
25
+ ConversationTokenCallbackType,
26
+ )
24
27
 
25
28
 
26
29
  logger = get_logger(__name__)
@@ -239,6 +242,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
239
242
  self,
240
243
  conversation: "LocalConversation",
241
244
  on_event: "ConversationCallbackType",
245
+ on_token: "ConversationTokenCallbackType | None" = None,
242
246
  ) -> None:
243
247
  """Taking a step in the conversation.
244
248
 
@@ -250,6 +254,9 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
250
254
  4.1 If conversation is finished, set state.execution_status to FINISHED
251
255
  4.2 Otherwise, just return, Conversation will kick off the next step
252
256
 
257
+ If the underlying LLM supports streaming, partial deltas are forwarded to
258
+ ``on_token`` before the full response is returned.
259
+
253
260
  NOTE: state will be mutated in-place.
254
261
  """
255
262
 
@@ -90,20 +90,10 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
90
90
  1. Step back and reflect on 5-7 different possible sources of the problem
91
91
  2. Assess the likelihood of each possible cause
92
92
  3. Methodically address the most likely causes, starting with the highest probability
93
- 4. Document your reasoning process
93
+ 4. Explain your reasoning process in your response to the user
94
94
  * When you run into any major issue while executing a plan from the user, please don't try to directly work around it. Instead, propose a new plan and confirm with the user before proceeding.
95
95
  </TROUBLESHOOTING>
96
96
 
97
- <DOCUMENTATION>
98
- * When explaining changes or solutions to the user:
99
- - Include explanations in your conversation responses rather than creating separate documentation files
100
- - If you need to create documentation files for reference, do NOT include them in version control unless explicitly requested
101
- - Never create multiple versions of documentation files with different suffixes
102
- * If the user asks for documentation:
103
- - Confirm whether they want it as a separate file or just in the conversation
104
- - Ask if they want documentation files to be included in version control
105
- </DOCUMENTATION>
106
-
107
97
  <PROCESS_MANAGEMENT>
108
98
  * When terminating processes:
109
99
  - Do NOT use general keywords with commands like `pkill -f server` or `pkill -f python` as this might accidentally kill other important servers or processes
@@ -12,6 +12,7 @@ from typing import (
12
12
 
13
13
  from openhands.sdk.context.condenser.base import CondenserBase
14
14
  from openhands.sdk.context.view import View
15
+ from openhands.sdk.conversation.types import ConversationTokenCallbackType
15
16
  from openhands.sdk.event.base import Event, LLMConvertibleEvent
16
17
  from openhands.sdk.event.condenser import Condensation
17
18
  from openhands.sdk.llm import LLM, LLMResponse, Message
@@ -182,6 +183,7 @@ def make_llm_completion(
182
183
  llm: LLM,
183
184
  messages: list[Message],
184
185
  tools: list[ToolDefinition] | None = None,
186
+ on_token: ConversationTokenCallbackType | None = None,
185
187
  ) -> LLMResponse:
186
188
  """Make an LLM completion call with the provided messages and tools.
187
189
 
@@ -189,6 +191,7 @@ def make_llm_completion(
189
191
  llm: The LLM instance to use for completion
190
192
  messages: The messages to send to the LLM
191
193
  tools: Optional list of tools to provide to the LLM
194
+ on_token: Optional callback for streaming token updates
192
195
 
193
196
  Returns:
194
197
  LLMResponse from the LLM completion call
@@ -200,10 +203,12 @@ def make_llm_completion(
200
203
  include=None,
201
204
  store=False,
202
205
  add_security_risk_prediction=True,
206
+ on_token=on_token,
203
207
  )
204
208
  else:
205
209
  return llm.completion(
206
210
  messages=messages,
207
211
  tools=tools or [],
208
212
  add_security_risk_prediction=True,
213
+ on_token=on_token,
209
214
  )
@@ -6,6 +6,7 @@ from openhands.sdk.context.prompts import render_template
6
6
  from openhands.sdk.context.skills import (
7
7
  Skill,
8
8
  SkillKnowledge,
9
+ load_public_skills,
9
10
  load_user_skills,
10
11
  )
11
12
  from openhands.sdk.llm import Message, TextContent
@@ -56,6 +57,14 @@ class AgentContext(BaseModel):
56
57
  "and ~/.openhands/microagents/ (for backward compatibility). "
57
58
  ),
58
59
  )
60
+ load_public_skills: bool = Field(
61
+ default=False,
62
+ description=(
63
+ "Whether to automatically load skills from the public OpenHands "
64
+ "skills repository at https://github.com/OpenHands/skills. "
65
+ "This allows you to get the latest skills without SDK updates."
66
+ ),
67
+ )
59
68
 
60
69
  @field_validator("skills")
61
70
  @classmethod
@@ -93,6 +102,27 @@ class AgentContext(BaseModel):
93
102
 
94
103
  return self
95
104
 
105
+ @model_validator(mode="after")
106
+ def _load_public_skills(self):
107
+ """Load public skills from OpenHands skills repository if enabled."""
108
+ if not self.load_public_skills:
109
+ return self
110
+ try:
111
+ public_skills = load_public_skills()
112
+ # Merge public skills with explicit skills, avoiding duplicates
113
+ existing_names = {skill.name for skill in self.skills}
114
+ for public_skill in public_skills:
115
+ if public_skill.name not in existing_names:
116
+ self.skills.append(public_skill)
117
+ else:
118
+ logger.warning(
119
+ f"Skipping public skill '{public_skill.name}' "
120
+ f"(already in existing skills)"
121
+ )
122
+ except Exception as e:
123
+ logger.warning(f"Failed to load public skills: {str(e)}")
124
+ return self
125
+
96
126
  def get_system_message_suffix(self) -> str | None:
97
127
  """Get the system message with repo skill content and custom suffix.
98
128
 
@@ -1,6 +1,7 @@
1
1
  from openhands.sdk.context.skills.exceptions import SkillValidationError
2
2
  from openhands.sdk.context.skills.skill import (
3
3
  Skill,
4
+ load_public_skills,
4
5
  load_skills_from_dir,
5
6
  load_user_skills,
6
7
  )
@@ -20,5 +21,6 @@ __all__ = [
20
21
  "SkillKnowledge",
21
22
  "load_skills_from_dir",
22
23
  "load_user_skills",
24
+ "load_public_skills",
23
25
  "SkillValidationError",
24
26
  ]
@@ -1,5 +1,7 @@
1
1
  import io
2
2
  import re
3
+ import shutil
4
+ import subprocess
3
5
  from itertools import chain
4
6
  from pathlib import Path
5
7
  from typing import Annotated, ClassVar, Union
@@ -15,10 +17,15 @@ from openhands.sdk.context.skills.trigger import (
15
17
  )
16
18
  from openhands.sdk.context.skills.types import InputMetadata
17
19
  from openhands.sdk.logger import get_logger
20
+ from openhands.sdk.utils import maybe_truncate
18
21
 
19
22
 
20
23
  logger = get_logger(__name__)
21
24
 
25
+ # Maximum characters for third-party skill files (e.g., AGENTS.md, CLAUDE.md, GEMINI.md)
26
+ # These files are always active, so we want to keep them reasonably sized
27
+ THIRD_PARTY_SKILL_MAX_CHARS = 10_000
28
+
22
29
  # Union type for all trigger types
23
30
  TriggerType = Annotated[
24
31
  KeywordTrigger | TaskTrigger,
@@ -71,6 +78,8 @@ class Skill(BaseModel):
71
78
  ".cursorrules": "cursorrules",
72
79
  "agents.md": "agents",
73
80
  "agent.md": "agents",
81
+ "claude.md": "claude",
82
+ "gemini.md": "gemini",
74
83
  }
75
84
 
76
85
  @classmethod
@@ -80,9 +89,30 @@ class Skill(BaseModel):
80
89
 
81
90
  # Create Skill with None trigger (always active) if we recognized the file type
82
91
  if skill_name is not None:
92
+ # Truncate content if it exceeds the limit
93
+ # Third-party files are always active, so we want to keep them
94
+ # reasonably sized
95
+ truncated_content = maybe_truncate(
96
+ file_content,
97
+ truncate_after=THIRD_PARTY_SKILL_MAX_CHARS,
98
+ truncate_notice=(
99
+ f"\n\n<TRUNCATED><NOTE>The file {path} exceeded the "
100
+ f"maximum length ({THIRD_PARTY_SKILL_MAX_CHARS} "
101
+ f"characters) and has been truncated. Only the "
102
+ f"beginning and end are shown. You can read the full "
103
+ f"file if needed.</NOTE>\n\n"
104
+ ),
105
+ )
106
+
107
+ if len(file_content) > THIRD_PARTY_SKILL_MAX_CHARS:
108
+ logger.warning(
109
+ f"Third-party skill file {path} ({len(file_content)} chars) "
110
+ f"exceeded limit ({THIRD_PARTY_SKILL_MAX_CHARS} chars), truncating"
111
+ )
112
+
83
113
  return Skill(
84
114
  name=skill_name,
85
- content=file_content,
115
+ content=truncated_content,
86
116
  source=str(path),
87
117
  trigger=None,
88
118
  )
@@ -366,3 +396,174 @@ def load_user_skills() -> list[Skill]:
366
396
  f"Loaded {len(all_skills)} user skills: {[s.name for s in all_skills]}"
367
397
  )
368
398
  return all_skills
399
+
400
+
401
+ # Public skills repository configuration
402
+ PUBLIC_SKILLS_REPO = "https://github.com/OpenHands/skills"
403
+ PUBLIC_SKILLS_BRANCH = "main"
404
+
405
+
406
+ def _get_skills_cache_dir() -> Path:
407
+ """Get the local cache directory for public skills repository.
408
+
409
+ Returns:
410
+ Path to the skills cache directory (~/.openhands/cache/skills).
411
+ """
412
+ cache_dir = Path.home() / ".openhands" / "cache" / "skills"
413
+ cache_dir.mkdir(parents=True, exist_ok=True)
414
+ return cache_dir
415
+
416
+
417
+ def _update_skills_repository(
418
+ repo_url: str,
419
+ branch: str,
420
+ cache_dir: Path,
421
+ ) -> Path | None:
422
+ """Clone or update the local skills repository.
423
+
424
+ Args:
425
+ repo_url: URL of the skills repository.
426
+ branch: Branch name to use.
427
+ cache_dir: Directory where the repository should be cached.
428
+
429
+ Returns:
430
+ Path to the local repository if successful, None otherwise.
431
+ """
432
+ repo_path = cache_dir / "public-skills"
433
+
434
+ try:
435
+ if repo_path.exists() and (repo_path / ".git").exists():
436
+ logger.debug(f"Updating skills repository at {repo_path}")
437
+ try:
438
+ subprocess.run(
439
+ ["git", "fetch", "origin"],
440
+ cwd=repo_path,
441
+ check=True,
442
+ capture_output=True,
443
+ timeout=30,
444
+ )
445
+ subprocess.run(
446
+ ["git", "reset", "--hard", f"origin/{branch}"],
447
+ cwd=repo_path,
448
+ check=True,
449
+ capture_output=True,
450
+ timeout=10,
451
+ )
452
+ logger.debug("Skills repository updated successfully")
453
+ except subprocess.TimeoutExpired:
454
+ logger.warning("Git pull timed out, using existing cached repository")
455
+ except subprocess.CalledProcessError as e:
456
+ logger.warning(
457
+ f"Failed to update repository: {e.stderr.decode()}, "
458
+ f"using existing cached version"
459
+ )
460
+ else:
461
+ logger.info(f"Cloning public skills repository from {repo_url}")
462
+ if repo_path.exists():
463
+ shutil.rmtree(repo_path)
464
+
465
+ subprocess.run(
466
+ [
467
+ "git",
468
+ "clone",
469
+ "--depth",
470
+ "1",
471
+ "--branch",
472
+ branch,
473
+ repo_url,
474
+ str(repo_path),
475
+ ],
476
+ check=True,
477
+ capture_output=True,
478
+ timeout=60,
479
+ )
480
+ logger.debug(f"Skills repository cloned to {repo_path}")
481
+
482
+ return repo_path
483
+
484
+ except subprocess.TimeoutExpired:
485
+ logger.warning(f"Git operation timed out for {repo_url}")
486
+ return None
487
+ except subprocess.CalledProcessError as e:
488
+ logger.warning(
489
+ f"Failed to clone/update repository {repo_url}: {e.stderr.decode()}"
490
+ )
491
+ return None
492
+ except Exception as e:
493
+ logger.warning(f"Error managing skills repository: {str(e)}")
494
+ return None
495
+
496
+
497
+ def load_public_skills(
498
+ repo_url: str = PUBLIC_SKILLS_REPO,
499
+ branch: str = PUBLIC_SKILLS_BRANCH,
500
+ ) -> list[Skill]:
501
+ """Load skills from the public OpenHands skills repository.
502
+
503
+ This function maintains a local git clone of the public skills registry at
504
+ https://github.com/OpenHands/skills. On first run, it clones the repository
505
+ to ~/.openhands/skills-cache/. On subsequent runs, it pulls the latest changes
506
+ to keep the skills up-to-date. This approach is more efficient than fetching
507
+ individual files via HTTP.
508
+
509
+ Args:
510
+ repo_url: URL of the skills repository. Defaults to the official
511
+ OpenHands skills repository.
512
+ branch: Branch name to load skills from. Defaults to 'main'.
513
+
514
+ Returns:
515
+ List of Skill objects loaded from the public repository.
516
+ Returns empty list if loading fails.
517
+
518
+ Example:
519
+ >>> from openhands.sdk.context import AgentContext
520
+ >>> from openhands.sdk.context.skills import load_public_skills
521
+ >>>
522
+ >>> # Load public skills
523
+ >>> public_skills = load_public_skills()
524
+ >>>
525
+ >>> # Use with AgentContext
526
+ >>> context = AgentContext(skills=public_skills)
527
+ """
528
+ all_skills = []
529
+
530
+ try:
531
+ # Get or update the local repository
532
+ cache_dir = _get_skills_cache_dir()
533
+ repo_path = _update_skills_repository(repo_url, branch, cache_dir)
534
+
535
+ if repo_path is None:
536
+ logger.warning("Failed to access public skills repository")
537
+ return all_skills
538
+
539
+ # Load skills from the local repository
540
+ skills_dir = repo_path / "skills"
541
+ if not skills_dir.exists():
542
+ logger.warning(f"Skills directory not found in repository: {skills_dir}")
543
+ return all_skills
544
+
545
+ # Find all .md files in the skills directory
546
+ md_files = [f for f in skills_dir.rglob("*.md") if f.name != "README.md"]
547
+
548
+ logger.info(f"Found {len(md_files)} skill files in public skills repository")
549
+
550
+ # Load each skill file
551
+ for skill_file in md_files:
552
+ try:
553
+ skill = Skill.load(
554
+ path=skill_file,
555
+ skill_dir=repo_path,
556
+ )
557
+ all_skills.append(skill)
558
+ logger.debug(f"Loaded public skill: {skill.name}")
559
+ except Exception as e:
560
+ logger.warning(f"Failed to load skill from {skill_file.name}: {str(e)}")
561
+ continue
562
+
563
+ except Exception as e:
564
+ logger.warning(f"Failed to load public skills from {repo_url}: {str(e)}")
565
+
566
+ logger.info(
567
+ f"Loaded {len(all_skills)} public skills: {[s.name for s in all_skills]}"
568
+ )
569
+ return all_skills
@@ -11,7 +11,10 @@ from openhands.sdk.conversation.state import (
11
11
  ConversationState,
12
12
  )
13
13
  from openhands.sdk.conversation.stuck_detector import StuckDetector
14
- from openhands.sdk.conversation.types import ConversationCallbackType
14
+ from openhands.sdk.conversation.types import (
15
+ ConversationCallbackType,
16
+ ConversationTokenCallbackType,
17
+ )
15
18
  from openhands.sdk.conversation.visualizer import (
16
19
  ConversationVisualizerBase,
17
20
  DefaultConversationVisualizer,
@@ -24,6 +27,7 @@ __all__ = [
24
27
  "ConversationState",
25
28
  "ConversationExecutionStatus",
26
29
  "ConversationCallbackType",
30
+ "ConversationTokenCallbackType",
27
31
  "DefaultConversationVisualizer",
28
32
  "ConversationVisualizerBase",
29
33
  "SecretRegistry",
@@ -1,12 +1,16 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from collections.abc import Iterable, Mapping
3
3
  from pathlib import Path
4
- from typing import TYPE_CHECKING, Protocol
4
+ from typing import TYPE_CHECKING, Protocol, TypeVar, cast
5
5
 
6
6
  from openhands.sdk.conversation.conversation_stats import ConversationStats
7
7
  from openhands.sdk.conversation.events_list_base import EventsListBase
8
8
  from openhands.sdk.conversation.secret_registry import SecretValue
9
- from openhands.sdk.conversation.types import ConversationCallbackType, ConversationID
9
+ from openhands.sdk.conversation.types import (
10
+ ConversationCallbackType,
11
+ ConversationID,
12
+ ConversationTokenCallbackType,
13
+ )
10
14
  from openhands.sdk.llm.llm import LLM
11
15
  from openhands.sdk.llm.message import Message
12
16
  from openhands.sdk.observability.laminar import (
@@ -27,6 +31,13 @@ if TYPE_CHECKING:
27
31
  from openhands.sdk.conversation.state import ConversationExecutionStatus
28
32
 
29
33
 
34
+ CallbackType = TypeVar(
35
+ "CallbackType",
36
+ ConversationCallbackType,
37
+ ConversationTokenCallbackType,
38
+ )
39
+
40
+
30
41
  class ConversationStateProtocol(Protocol):
31
42
  """Protocol defining the interface for conversation state objects."""
32
43
 
@@ -235,9 +246,7 @@ class BaseConversation(ABC):
235
246
  ...
236
247
 
237
248
  @staticmethod
238
- def compose_callbacks(
239
- callbacks: Iterable[ConversationCallbackType],
240
- ) -> ConversationCallbackType:
249
+ def compose_callbacks(callbacks: Iterable[CallbackType]) -> CallbackType:
241
250
  """Compose multiple callbacks into a single callback function.
242
251
 
243
252
  Args:
@@ -252,4 +261,4 @@ class BaseConversation(ABC):
252
261
  if cb:
253
262
  cb(event)
254
263
 
255
- return composed
264
+ return cast(CallbackType, composed)
@@ -4,7 +4,11 @@ from typing import TYPE_CHECKING, Self, overload
4
4
  from openhands.sdk.agent.base import AgentBase
5
5
  from openhands.sdk.conversation.base import BaseConversation
6
6
  from openhands.sdk.conversation.secret_registry import SecretValue
7
- from openhands.sdk.conversation.types import ConversationCallbackType, ConversationID
7
+ from openhands.sdk.conversation.types import (
8
+ ConversationCallbackType,
9
+ ConversationID,
10
+ ConversationTokenCallbackType,
11
+ )
8
12
  from openhands.sdk.conversation.visualizer import (
9
13
  ConversationVisualizerBase,
10
14
  DefaultConversationVisualizer,
@@ -49,6 +53,7 @@ class Conversation:
49
53
  persistence_dir: str | Path | None = None,
50
54
  conversation_id: ConversationID | None = None,
51
55
  callbacks: list[ConversationCallbackType] | None = None,
56
+ token_callbacks: list[ConversationTokenCallbackType] | None = None,
52
57
  max_iteration_per_run: int = 500,
53
58
  stuck_detection: bool = True,
54
59
  visualizer: (
@@ -65,6 +70,7 @@ class Conversation:
65
70
  workspace: RemoteWorkspace,
66
71
  conversation_id: ConversationID | None = None,
67
72
  callbacks: list[ConversationCallbackType] | None = None,
73
+ token_callbacks: list[ConversationTokenCallbackType] | None = None,
68
74
  max_iteration_per_run: int = 500,
69
75
  stuck_detection: bool = True,
70
76
  visualizer: (
@@ -81,6 +87,7 @@ class Conversation:
81
87
  persistence_dir: str | Path | None = None,
82
88
  conversation_id: ConversationID | None = None,
83
89
  callbacks: list[ConversationCallbackType] | None = None,
90
+ token_callbacks: list[ConversationTokenCallbackType] | None = None,
84
91
  max_iteration_per_run: int = 500,
85
92
  stuck_detection: bool = True,
86
93
  visualizer: (
@@ -104,6 +111,7 @@ class Conversation:
104
111
  agent=agent,
105
112
  conversation_id=conversation_id,
106
113
  callbacks=callbacks,
114
+ token_callbacks=token_callbacks,
107
115
  max_iteration_per_run=max_iteration_per_run,
108
116
  stuck_detection=stuck_detection,
109
117
  visualizer=visualizer,
@@ -115,6 +123,7 @@ class Conversation:
115
123
  agent=agent,
116
124
  conversation_id=conversation_id,
117
125
  callbacks=callbacks,
126
+ token_callbacks=token_callbacks,
118
127
  max_iteration_per_run=max_iteration_per_run,
119
128
  stuck_detection=stuck_detection,
120
129
  visualizer=visualizer,