openhands-sdk 1.7.3__py3-none-any.whl

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 (180) hide show
  1. openhands/sdk/__init__.py +111 -0
  2. openhands/sdk/agent/__init__.py +8 -0
  3. openhands/sdk/agent/agent.py +650 -0
  4. openhands/sdk/agent/base.py +457 -0
  5. openhands/sdk/agent/prompts/in_context_learning_example.j2 +169 -0
  6. openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +3 -0
  7. openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
  8. openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
  9. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +2 -0
  10. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
  11. openhands/sdk/agent/prompts/security_policy.j2 +22 -0
  12. openhands/sdk/agent/prompts/security_risk_assessment.j2 +21 -0
  13. openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
  14. openhands/sdk/agent/prompts/system_prompt.j2 +132 -0
  15. openhands/sdk/agent/prompts/system_prompt_interactive.j2 +14 -0
  16. openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +40 -0
  17. openhands/sdk/agent/prompts/system_prompt_planning.j2 +40 -0
  18. openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +122 -0
  19. openhands/sdk/agent/utils.py +228 -0
  20. openhands/sdk/context/__init__.py +28 -0
  21. openhands/sdk/context/agent_context.py +264 -0
  22. openhands/sdk/context/condenser/__init__.py +18 -0
  23. openhands/sdk/context/condenser/base.py +100 -0
  24. openhands/sdk/context/condenser/llm_summarizing_condenser.py +248 -0
  25. openhands/sdk/context/condenser/no_op_condenser.py +14 -0
  26. openhands/sdk/context/condenser/pipeline_condenser.py +56 -0
  27. openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +59 -0
  28. openhands/sdk/context/condenser/utils.py +149 -0
  29. openhands/sdk/context/prompts/__init__.py +6 -0
  30. openhands/sdk/context/prompts/prompt.py +114 -0
  31. openhands/sdk/context/prompts/templates/ask_agent_template.j2 +11 -0
  32. openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +8 -0
  33. openhands/sdk/context/prompts/templates/system_message_suffix.j2 +32 -0
  34. openhands/sdk/context/skills/__init__.py +28 -0
  35. openhands/sdk/context/skills/exceptions.py +11 -0
  36. openhands/sdk/context/skills/skill.py +720 -0
  37. openhands/sdk/context/skills/trigger.py +36 -0
  38. openhands/sdk/context/skills/types.py +48 -0
  39. openhands/sdk/context/view.py +503 -0
  40. openhands/sdk/conversation/__init__.py +40 -0
  41. openhands/sdk/conversation/base.py +281 -0
  42. openhands/sdk/conversation/conversation.py +152 -0
  43. openhands/sdk/conversation/conversation_stats.py +85 -0
  44. openhands/sdk/conversation/event_store.py +157 -0
  45. openhands/sdk/conversation/events_list_base.py +17 -0
  46. openhands/sdk/conversation/exceptions.py +50 -0
  47. openhands/sdk/conversation/fifo_lock.py +133 -0
  48. openhands/sdk/conversation/impl/__init__.py +5 -0
  49. openhands/sdk/conversation/impl/local_conversation.py +665 -0
  50. openhands/sdk/conversation/impl/remote_conversation.py +956 -0
  51. openhands/sdk/conversation/persistence_const.py +9 -0
  52. openhands/sdk/conversation/response_utils.py +41 -0
  53. openhands/sdk/conversation/secret_registry.py +126 -0
  54. openhands/sdk/conversation/serialization_diff.py +0 -0
  55. openhands/sdk/conversation/state.py +392 -0
  56. openhands/sdk/conversation/stuck_detector.py +311 -0
  57. openhands/sdk/conversation/title_utils.py +191 -0
  58. openhands/sdk/conversation/types.py +45 -0
  59. openhands/sdk/conversation/visualizer/__init__.py +12 -0
  60. openhands/sdk/conversation/visualizer/base.py +67 -0
  61. openhands/sdk/conversation/visualizer/default.py +373 -0
  62. openhands/sdk/critic/__init__.py +15 -0
  63. openhands/sdk/critic/base.py +38 -0
  64. openhands/sdk/critic/impl/__init__.py +12 -0
  65. openhands/sdk/critic/impl/agent_finished.py +83 -0
  66. openhands/sdk/critic/impl/empty_patch.py +49 -0
  67. openhands/sdk/critic/impl/pass_critic.py +42 -0
  68. openhands/sdk/event/__init__.py +42 -0
  69. openhands/sdk/event/base.py +149 -0
  70. openhands/sdk/event/condenser.py +82 -0
  71. openhands/sdk/event/conversation_error.py +25 -0
  72. openhands/sdk/event/conversation_state.py +104 -0
  73. openhands/sdk/event/llm_completion_log.py +39 -0
  74. openhands/sdk/event/llm_convertible/__init__.py +20 -0
  75. openhands/sdk/event/llm_convertible/action.py +139 -0
  76. openhands/sdk/event/llm_convertible/message.py +142 -0
  77. openhands/sdk/event/llm_convertible/observation.py +141 -0
  78. openhands/sdk/event/llm_convertible/system.py +61 -0
  79. openhands/sdk/event/token.py +16 -0
  80. openhands/sdk/event/types.py +11 -0
  81. openhands/sdk/event/user_action.py +21 -0
  82. openhands/sdk/git/exceptions.py +43 -0
  83. openhands/sdk/git/git_changes.py +249 -0
  84. openhands/sdk/git/git_diff.py +129 -0
  85. openhands/sdk/git/models.py +21 -0
  86. openhands/sdk/git/utils.py +189 -0
  87. openhands/sdk/hooks/__init__.py +30 -0
  88. openhands/sdk/hooks/config.py +180 -0
  89. openhands/sdk/hooks/conversation_hooks.py +227 -0
  90. openhands/sdk/hooks/executor.py +155 -0
  91. openhands/sdk/hooks/manager.py +170 -0
  92. openhands/sdk/hooks/types.py +40 -0
  93. openhands/sdk/io/__init__.py +6 -0
  94. openhands/sdk/io/base.py +48 -0
  95. openhands/sdk/io/cache.py +85 -0
  96. openhands/sdk/io/local.py +119 -0
  97. openhands/sdk/io/memory.py +54 -0
  98. openhands/sdk/llm/__init__.py +45 -0
  99. openhands/sdk/llm/exceptions/__init__.py +45 -0
  100. openhands/sdk/llm/exceptions/classifier.py +50 -0
  101. openhands/sdk/llm/exceptions/mapping.py +54 -0
  102. openhands/sdk/llm/exceptions/types.py +101 -0
  103. openhands/sdk/llm/llm.py +1140 -0
  104. openhands/sdk/llm/llm_registry.py +122 -0
  105. openhands/sdk/llm/llm_response.py +59 -0
  106. openhands/sdk/llm/message.py +656 -0
  107. openhands/sdk/llm/mixins/fn_call_converter.py +1288 -0
  108. openhands/sdk/llm/mixins/non_native_fc.py +97 -0
  109. openhands/sdk/llm/options/__init__.py +1 -0
  110. openhands/sdk/llm/options/chat_options.py +93 -0
  111. openhands/sdk/llm/options/common.py +19 -0
  112. openhands/sdk/llm/options/responses_options.py +67 -0
  113. openhands/sdk/llm/router/__init__.py +10 -0
  114. openhands/sdk/llm/router/base.py +117 -0
  115. openhands/sdk/llm/router/impl/multimodal.py +76 -0
  116. openhands/sdk/llm/router/impl/random.py +22 -0
  117. openhands/sdk/llm/streaming.py +9 -0
  118. openhands/sdk/llm/utils/metrics.py +312 -0
  119. openhands/sdk/llm/utils/model_features.py +192 -0
  120. openhands/sdk/llm/utils/model_info.py +90 -0
  121. openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
  122. openhands/sdk/llm/utils/retry_mixin.py +128 -0
  123. openhands/sdk/llm/utils/telemetry.py +362 -0
  124. openhands/sdk/llm/utils/unverified_models.py +156 -0
  125. openhands/sdk/llm/utils/verified_models.py +65 -0
  126. openhands/sdk/logger/__init__.py +22 -0
  127. openhands/sdk/logger/logger.py +195 -0
  128. openhands/sdk/logger/rolling.py +113 -0
  129. openhands/sdk/mcp/__init__.py +24 -0
  130. openhands/sdk/mcp/client.py +76 -0
  131. openhands/sdk/mcp/definition.py +106 -0
  132. openhands/sdk/mcp/exceptions.py +19 -0
  133. openhands/sdk/mcp/tool.py +270 -0
  134. openhands/sdk/mcp/utils.py +83 -0
  135. openhands/sdk/observability/__init__.py +4 -0
  136. openhands/sdk/observability/laminar.py +166 -0
  137. openhands/sdk/observability/utils.py +20 -0
  138. openhands/sdk/py.typed +0 -0
  139. openhands/sdk/secret/__init__.py +19 -0
  140. openhands/sdk/secret/secrets.py +92 -0
  141. openhands/sdk/security/__init__.py +6 -0
  142. openhands/sdk/security/analyzer.py +111 -0
  143. openhands/sdk/security/confirmation_policy.py +61 -0
  144. openhands/sdk/security/llm_analyzer.py +29 -0
  145. openhands/sdk/security/risk.py +100 -0
  146. openhands/sdk/tool/__init__.py +34 -0
  147. openhands/sdk/tool/builtins/__init__.py +34 -0
  148. openhands/sdk/tool/builtins/finish.py +106 -0
  149. openhands/sdk/tool/builtins/think.py +117 -0
  150. openhands/sdk/tool/registry.py +184 -0
  151. openhands/sdk/tool/schema.py +286 -0
  152. openhands/sdk/tool/spec.py +39 -0
  153. openhands/sdk/tool/tool.py +481 -0
  154. openhands/sdk/utils/__init__.py +22 -0
  155. openhands/sdk/utils/async_executor.py +115 -0
  156. openhands/sdk/utils/async_utils.py +39 -0
  157. openhands/sdk/utils/cipher.py +68 -0
  158. openhands/sdk/utils/command.py +90 -0
  159. openhands/sdk/utils/deprecation.py +166 -0
  160. openhands/sdk/utils/github.py +44 -0
  161. openhands/sdk/utils/json.py +48 -0
  162. openhands/sdk/utils/models.py +570 -0
  163. openhands/sdk/utils/paging.py +63 -0
  164. openhands/sdk/utils/pydantic_diff.py +85 -0
  165. openhands/sdk/utils/pydantic_secrets.py +64 -0
  166. openhands/sdk/utils/truncate.py +117 -0
  167. openhands/sdk/utils/visualize.py +58 -0
  168. openhands/sdk/workspace/__init__.py +17 -0
  169. openhands/sdk/workspace/base.py +158 -0
  170. openhands/sdk/workspace/local.py +189 -0
  171. openhands/sdk/workspace/models.py +35 -0
  172. openhands/sdk/workspace/remote/__init__.py +8 -0
  173. openhands/sdk/workspace/remote/async_remote_workspace.py +149 -0
  174. openhands/sdk/workspace/remote/base.py +164 -0
  175. openhands/sdk/workspace/remote/remote_workspace_mixin.py +323 -0
  176. openhands/sdk/workspace/workspace.py +49 -0
  177. openhands_sdk-1.7.3.dist-info/METADATA +17 -0
  178. openhands_sdk-1.7.3.dist-info/RECORD +180 -0
  179. openhands_sdk-1.7.3.dist-info/WHEEL +5 -0
  180. openhands_sdk-1.7.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,111 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from openhands.sdk.event.base import Event
4
+ from openhands.sdk.event.llm_convertible import ActionEvent
5
+ from openhands.sdk.logger import get_logger
6
+ from openhands.sdk.security.risk import SecurityRisk
7
+ from openhands.sdk.utils.models import (
8
+ DiscriminatedUnionMixin,
9
+ )
10
+
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ class SecurityAnalyzerBase(DiscriminatedUnionMixin, ABC):
16
+ """Abstract base class for security analyzers.
17
+
18
+ Security analyzers evaluate the risk of actions before they are executed
19
+ and can influence the conversation flow based on security policies.
20
+
21
+ This is adapted from OpenHands SecurityAnalyzer but designed to work
22
+ with the agent-sdk's conversation-based architecture.
23
+ """
24
+
25
+ @abstractmethod
26
+ def security_risk(self, action: ActionEvent) -> SecurityRisk:
27
+ """Evaluate the security risk of an ActionEvent.
28
+
29
+ This is the core method that analyzes an ActionEvent and returns its risk level.
30
+ Implementations should examine the action's content, context, and potential
31
+ impact to determine the appropriate risk level.
32
+
33
+ Args:
34
+ action: The ActionEvent to analyze for security risks
35
+
36
+ Returns:
37
+ ActionSecurityRisk enum indicating the risk level
38
+ """
39
+ pass
40
+
41
+ def analyze_event(self, event: Event) -> SecurityRisk | None:
42
+ """Analyze an event for security risks.
43
+
44
+ This is a convenience method that checks if the event is an action
45
+ and calls security_risk() if it is. Non-action events return None.
46
+
47
+ Args:
48
+ event: The event to analyze
49
+
50
+ Returns:
51
+ ActionSecurityRisk if event is an action, None otherwise
52
+ """
53
+ if isinstance(event, ActionEvent):
54
+ return self.security_risk(event)
55
+ return None
56
+
57
+ def should_require_confirmation(
58
+ self, risk: SecurityRisk, confirmation_mode: bool = False
59
+ ) -> bool:
60
+ """Determine if an action should require user confirmation.
61
+
62
+ This implements the default confirmation logic based on risk level
63
+ and confirmation mode settings.
64
+
65
+ Args:
66
+ risk: The security risk level of the action
67
+ confirmation_mode: Whether confirmation mode is enabled
68
+
69
+ Returns:
70
+ True if confirmation is required, False otherwise
71
+ """
72
+ if risk == SecurityRisk.HIGH:
73
+ # HIGH risk actions always require confirmation
74
+ return True
75
+ elif risk == SecurityRisk.UNKNOWN and not confirmation_mode:
76
+ # UNKNOWN risk requires confirmation if no security analyzer is configured
77
+ return True
78
+ elif confirmation_mode:
79
+ # In confirmation mode, all actions require confirmation
80
+ return True
81
+ else:
82
+ # LOW and MEDIUM risk actions don't require confirmation by default
83
+ return False
84
+
85
+ def analyze_pending_actions(
86
+ self, pending_actions: list[ActionEvent]
87
+ ) -> list[tuple[ActionEvent, SecurityRisk]]:
88
+ """Analyze all pending actions in a conversation.
89
+
90
+ This method gets all unmatched actions from the conversation state
91
+ and analyzes each one for security risks.
92
+
93
+ Args:
94
+ conversation: The conversation to analyze
95
+
96
+ Returns:
97
+ List of tuples containing (action, risk_level) for each pending action
98
+ """
99
+ analyzed_actions = []
100
+
101
+ for action_event in pending_actions:
102
+ try:
103
+ risk = self.security_risk(action_event)
104
+ analyzed_actions.append((action_event, risk))
105
+ logger.debug(f"Action {action_event} analyzed with risk level: {risk}")
106
+ except Exception as e:
107
+ logger.error(f"Error analyzing action {action_event}: {e}")
108
+ # Default to HIGH risk on analysis error for safety
109
+ analyzed_actions.append((action_event, SecurityRisk.HIGH))
110
+
111
+ return analyzed_actions
@@ -0,0 +1,61 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from pydantic import field_validator
4
+
5
+ from openhands.sdk.security.risk import SecurityRisk
6
+ from openhands.sdk.utils.models import DiscriminatedUnionMixin
7
+
8
+
9
+ class ConfirmationPolicyBase(DiscriminatedUnionMixin, ABC):
10
+ @abstractmethod
11
+ def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) -> bool:
12
+ """Determine if an action with the given risk level requires confirmation.
13
+
14
+ This method defines the core logic for determining whether user confirmation
15
+ is required before executing an action based on its security risk level.
16
+
17
+ Args:
18
+ risk: The security risk level of the action to be evaluated.
19
+ Defaults to SecurityRisk.UNKNOWN if not specified.
20
+
21
+ Returns:
22
+ True if the action requires user confirmation before execution,
23
+ False if the action can proceed without confirmation.
24
+ """
25
+
26
+
27
+ class AlwaysConfirm(ConfirmationPolicyBase):
28
+ def should_confirm(
29
+ self,
30
+ risk: SecurityRisk = SecurityRisk.UNKNOWN, # noqa: ARG002
31
+ ) -> bool:
32
+ return True
33
+
34
+
35
+ class NeverConfirm(ConfirmationPolicyBase):
36
+ def should_confirm(
37
+ self,
38
+ risk: SecurityRisk = SecurityRisk.UNKNOWN, # noqa: ARG002
39
+ ) -> bool:
40
+ return False
41
+
42
+
43
+ class ConfirmRisky(ConfirmationPolicyBase):
44
+ threshold: SecurityRisk = SecurityRisk.HIGH
45
+ confirm_unknown: bool = True
46
+
47
+ @field_validator("threshold")
48
+ def validate_threshold(cls, v: SecurityRisk) -> SecurityRisk:
49
+ if v == SecurityRisk.UNKNOWN:
50
+ raise ValueError("Threshold cannot be UNKNOWN")
51
+ return v
52
+
53
+ def should_confirm(self, risk: SecurityRisk = SecurityRisk.UNKNOWN) -> bool:
54
+ if risk == SecurityRisk.UNKNOWN:
55
+ return self.confirm_unknown
56
+
57
+ # This comparison is reflexive by default, so if the threshold is HIGH we will
58
+ # still require confirmation for HIGH risk actions. And since the threshold is
59
+ # guaranteed to never be UNKNOWN (by the validator), we're guaranteed to get a
60
+ # boolean here.
61
+ return risk.is_riskier(self.threshold)
@@ -0,0 +1,29 @@
1
+ from openhands.sdk.event import ActionEvent
2
+ from openhands.sdk.logger import get_logger
3
+ from openhands.sdk.security.analyzer import SecurityAnalyzerBase
4
+ from openhands.sdk.security.risk import SecurityRisk
5
+
6
+
7
+ logger = get_logger(__name__)
8
+
9
+
10
+ class LLMSecurityAnalyzer(SecurityAnalyzerBase):
11
+ """LLM-based security analyzer.
12
+
13
+ This analyzer respects the security_risk attribute that can be set by the LLM
14
+ when generating actions, similar to OpenHands' LLMRiskAnalyzer.
15
+
16
+ It provides a lightweight security analysis approach that leverages the LLM's
17
+ understanding of action context and potential risks.
18
+ """
19
+
20
+ def security_risk(self, action: ActionEvent) -> SecurityRisk:
21
+ """Evaluate security risk based on LLM-provided assessment.
22
+
23
+ This method checks if the action has a security_risk attribute set by the LLM
24
+ and returns it. The LLM may not always provide this attribute but it defaults to
25
+ UNKNOWN if not explicitly set.
26
+ """
27
+ logger.debug(f"Analyzing security risk: {action} -- {action.security_risk}")
28
+
29
+ return action.security_risk
@@ -0,0 +1,100 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+ from rich.text import Text
6
+
7
+
8
+ class SecurityRisk(str, Enum):
9
+ """Security risk levels for actions.
10
+
11
+ Based on OpenHands security risk levels but adapted for agent-sdk.
12
+ Integer values allow for easy comparison and ordering.
13
+ """
14
+
15
+ UNKNOWN = "UNKNOWN"
16
+ LOW = "LOW"
17
+ MEDIUM = "MEDIUM"
18
+ HIGH = "HIGH"
19
+
20
+ @property
21
+ def description(self) -> str:
22
+ """Get a human-readable description of the risk level."""
23
+ descriptions = {
24
+ SecurityRisk.LOW: (
25
+ "Low risk - Safe operation with minimal security impact"
26
+ ),
27
+ SecurityRisk.MEDIUM: (
28
+ "Medium risk - Moderate security impact, review recommended"
29
+ ),
30
+ SecurityRisk.HIGH: (
31
+ "High risk - Significant security impact, confirmation required"
32
+ ),
33
+ SecurityRisk.UNKNOWN: ("Unknown risk - Risk level could not be determined"),
34
+ }
35
+ return descriptions.get(self, "Unknown risk level")
36
+
37
+ def __str__(self) -> str:
38
+ return self.name
39
+
40
+ def get_color(self) -> str:
41
+ """Get the color for displaying this risk level in Rich text."""
42
+ color_map = {
43
+ SecurityRisk.LOW: "green",
44
+ SecurityRisk.MEDIUM: "yellow",
45
+ SecurityRisk.HIGH: "red",
46
+ SecurityRisk.UNKNOWN: "white",
47
+ }
48
+ return color_map.get(self, "white")
49
+
50
+ @property
51
+ def visualize(self) -> Text:
52
+ """Return Rich Text representation of this risk level."""
53
+ content = Text()
54
+ content.append(
55
+ "Predicted Security Risk: ",
56
+ style="bold",
57
+ )
58
+ content.append(
59
+ f"{self.value}\n\n",
60
+ style=f"bold {self.get_color()}",
61
+ )
62
+ return content
63
+
64
+ def is_riskier(self, other: SecurityRisk, reflexive: bool = True) -> bool:
65
+ """Check if this risk level is riskier than another.
66
+
67
+ Risk levels follow the natural ordering: LOW is less risky than MEDIUM, which is
68
+ less risky than HIGH. UNKNOWN is not comparable to any other level.
69
+
70
+ To make this act like a standard well-ordered domain, we reflexively consider
71
+ risk levels to be riskier than themselves. That is:
72
+
73
+ for risk_level in list(SecurityRisk):
74
+ assert risk_level.is_riskier(risk_level)
75
+
76
+ # More concretely:
77
+ assert SecurityRisk.HIGH.is_riskier(SecurityRisk.HIGH)
78
+ assert SecurityRisk.MEDIUM.is_riskier(SecurityRisk.MEDIUM)
79
+ assert SecurityRisk.LOW.is_riskier(SecurityRisk.LOW)
80
+
81
+ This can be disabled by setting the `reflexive` parameter to False.
82
+
83
+ Args:
84
+ other (SecurityRisk): The other risk level to compare against.
85
+ reflexive (bool): Whether the relationship is reflexive.
86
+
87
+ Raises:
88
+ ValueError: If either risk level is UNKNOWN.
89
+ """
90
+ if self.value == SecurityRisk.UNKNOWN or other.value == SecurityRisk.UNKNOWN:
91
+ raise ValueError("Cannot compare unknown risk levels.")
92
+
93
+ # Map risk levels to a well-ordered domain for comparison. No need to map
94
+ # UNKNOWN since we'll already have raised an error by now if either is UNKNOWN.
95
+ risk_order = {
96
+ SecurityRisk.LOW: 1,
97
+ SecurityRisk.MEDIUM: 2,
98
+ SecurityRisk.HIGH: 3,
99
+ }
100
+ return risk_order[self] > risk_order[other] or (reflexive and self == other)
@@ -0,0 +1,34 @@
1
+ from openhands.sdk.tool.builtins import BUILT_IN_TOOLS, FinishTool, ThinkTool
2
+ from openhands.sdk.tool.registry import (
3
+ list_registered_tools,
4
+ register_tool,
5
+ resolve_tool,
6
+ )
7
+ from openhands.sdk.tool.schema import (
8
+ Action,
9
+ Observation,
10
+ )
11
+ from openhands.sdk.tool.spec import Tool
12
+ from openhands.sdk.tool.tool import (
13
+ ExecutableTool,
14
+ ToolAnnotations,
15
+ ToolDefinition,
16
+ ToolExecutor,
17
+ )
18
+
19
+
20
+ __all__ = [
21
+ "Tool",
22
+ "ToolDefinition",
23
+ "ToolAnnotations",
24
+ "ToolExecutor",
25
+ "ExecutableTool",
26
+ "Action",
27
+ "Observation",
28
+ "FinishTool",
29
+ "ThinkTool",
30
+ "BUILT_IN_TOOLS",
31
+ "register_tool",
32
+ "resolve_tool",
33
+ "list_registered_tools",
34
+ ]
@@ -0,0 +1,34 @@
1
+ """Implementing essential tools that doesn't interact with the environment.
2
+
3
+ These are built in and are *required* for the agent to work.
4
+
5
+ For tools that require interacting with the environment, add them to `openhands-tools`.
6
+ """
7
+
8
+ from openhands.sdk.tool.builtins.finish import (
9
+ FinishAction,
10
+ FinishExecutor,
11
+ FinishObservation,
12
+ FinishTool,
13
+ )
14
+ from openhands.sdk.tool.builtins.think import (
15
+ ThinkAction,
16
+ ThinkExecutor,
17
+ ThinkObservation,
18
+ ThinkTool,
19
+ )
20
+
21
+
22
+ BUILT_IN_TOOLS = [FinishTool, ThinkTool]
23
+
24
+ __all__ = [
25
+ "BUILT_IN_TOOLS",
26
+ "FinishTool",
27
+ "FinishAction",
28
+ "FinishObservation",
29
+ "FinishExecutor",
30
+ "ThinkTool",
31
+ "ThinkAction",
32
+ "ThinkObservation",
33
+ "ThinkExecutor",
34
+ ]
@@ -0,0 +1,106 @@
1
+ from collections.abc import Sequence
2
+ from typing import TYPE_CHECKING, Self
3
+
4
+ from pydantic import Field
5
+ from rich.text import Text
6
+
7
+ from openhands.sdk.tool.tool import (
8
+ Action,
9
+ Observation,
10
+ ToolAnnotations,
11
+ ToolDefinition,
12
+ ToolExecutor,
13
+ )
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from openhands.sdk.conversation.base import BaseConversation
18
+ from openhands.sdk.conversation.state import ConversationState
19
+
20
+
21
+ class FinishAction(Action):
22
+ message: str = Field(description="Final message to send to the user.")
23
+
24
+ @property
25
+ def visualize(self) -> Text:
26
+ """Return Rich Text representation of this action."""
27
+ content = Text()
28
+ content.append("Finish with message:\n", style="bold blue")
29
+ content.append(self.message)
30
+ return content
31
+
32
+
33
+ class FinishObservation(Observation):
34
+ """
35
+ Observation returned after finishing a task.
36
+ The FinishAction itself contains the message sent to the user so no
37
+ extra fields are needed here.
38
+ """
39
+
40
+ @property
41
+ def visualize(self) -> Text:
42
+ """Return an empty Text representation since the message is in the action."""
43
+ return Text()
44
+
45
+
46
+ TOOL_DESCRIPTION = """Signals the completion of the current task or conversation.
47
+
48
+ Use this tool when:
49
+ - You have successfully completed the user's requested task
50
+ - You cannot proceed further due to technical limitations or missing information
51
+
52
+ The message should include:
53
+ - A clear summary of actions taken and their results
54
+ - Any next steps for the user
55
+ - Explanation if you're unable to complete the task
56
+ - Any follow-up questions if more information is needed
57
+ """
58
+
59
+
60
+ class FinishExecutor(ToolExecutor):
61
+ def __call__(
62
+ self,
63
+ action: FinishAction,
64
+ conversation: "BaseConversation | None" = None, # noqa: ARG002
65
+ ) -> FinishObservation:
66
+ return FinishObservation.from_text(text=action.message)
67
+
68
+
69
+ class FinishTool(ToolDefinition[FinishAction, FinishObservation]):
70
+ """Tool for signaling the completion of a task or conversation."""
71
+
72
+ @classmethod
73
+ def create(
74
+ cls,
75
+ conv_state: "ConversationState | None" = None, # noqa: ARG003
76
+ **params,
77
+ ) -> Sequence[Self]:
78
+ """Create FinishTool instance.
79
+
80
+ Args:
81
+ conv_state: Optional conversation state (not used by FinishTool).
82
+ **params: Additional parameters (none supported).
83
+
84
+ Returns:
85
+ A sequence containing a single FinishTool instance.
86
+
87
+ Raises:
88
+ ValueError: If any parameters are provided.
89
+ """
90
+ if params:
91
+ raise ValueError("FinishTool doesn't accept parameters")
92
+ return [
93
+ cls(
94
+ action_type=FinishAction,
95
+ observation_type=FinishObservation,
96
+ description=TOOL_DESCRIPTION,
97
+ executor=FinishExecutor(),
98
+ annotations=ToolAnnotations(
99
+ title="finish",
100
+ readOnlyHint=True,
101
+ destructiveHint=False,
102
+ idempotentHint=True,
103
+ openWorldHint=False,
104
+ ),
105
+ )
106
+ ]
@@ -0,0 +1,117 @@
1
+ from collections.abc import Sequence
2
+ from typing import TYPE_CHECKING, Self
3
+
4
+ from pydantic import Field
5
+ from rich.text import Text
6
+
7
+ from openhands.sdk.tool.tool import (
8
+ Action,
9
+ Observation,
10
+ ToolAnnotations,
11
+ ToolDefinition,
12
+ ToolExecutor,
13
+ )
14
+
15
+
16
+ if TYPE_CHECKING:
17
+ from openhands.sdk.conversation.base import BaseConversation
18
+ from openhands.sdk.conversation.state import ConversationState
19
+
20
+
21
+ class ThinkAction(Action):
22
+ """Action for logging a thought without making any changes."""
23
+
24
+ thought: str = Field(description="The thought to log.")
25
+
26
+ @property
27
+ def visualize(self) -> Text:
28
+ """Return Rich Text representation with thinking styling."""
29
+ content = Text()
30
+
31
+ # Add thinking icon and header
32
+ content.append("🤔 ", style="yellow")
33
+ content.append("Thinking: ", style="bold yellow")
34
+
35
+ # Add the thought content with proper formatting
36
+ if self.thought:
37
+ # Split into lines for better formatting
38
+ lines = self.thought.split("\n")
39
+ for i, line in enumerate(lines):
40
+ if i > 0:
41
+ content.append("\n")
42
+ content.append(line.strip(), style="italic white")
43
+
44
+ return content
45
+
46
+
47
+ class ThinkObservation(Observation):
48
+ """
49
+ Observation returned after logging a thought.
50
+ The ThinkAction itself contains the thought logged so no extra
51
+ fields are needed here.
52
+ """
53
+
54
+ @property
55
+ def visualize(self) -> Text:
56
+ """Return an empty Text representation since the thought is in the action."""
57
+ return Text()
58
+
59
+
60
+ THINK_DESCRIPTION = """Use the tool to think about something. It will not obtain new information or make any changes to the repository, but just log the thought. Use it when complex reasoning or brainstorming is needed.
61
+
62
+ Common use cases:
63
+ 1. When exploring a repository and discovering the source of a bug, call this tool to brainstorm several unique ways of fixing the bug, and assess which change(s) are likely to be simplest and most effective.
64
+ 2. After receiving test results, use this tool to brainstorm ways to fix failing tests.
65
+ 3. When planning a complex refactoring, use this tool to outline different approaches and their tradeoffs.
66
+ 4. When designing a new feature, use this tool to think through architecture decisions and implementation details.
67
+ 5. When debugging a complex issue, use this tool to organize your thoughts and hypotheses.
68
+
69
+ The tool simply logs your thought process for better transparency and does not execute any code or make changes.""" # noqa: E501
70
+
71
+
72
+ class ThinkExecutor(ToolExecutor):
73
+ def __call__(
74
+ self,
75
+ _: ThinkAction,
76
+ conversation: "BaseConversation | None" = None, # noqa: ARG002
77
+ ) -> ThinkObservation:
78
+ return ThinkObservation.from_text(text="Your thought has been logged.")
79
+
80
+
81
+ class ThinkTool(ToolDefinition[ThinkAction, ThinkObservation]):
82
+ """Tool for logging thoughts without making changes."""
83
+
84
+ @classmethod
85
+ def create(
86
+ cls,
87
+ conv_state: "ConversationState | None" = None, # noqa: ARG003
88
+ **params,
89
+ ) -> Sequence[Self]:
90
+ """Create ThinkTool instance.
91
+
92
+ Args:
93
+ conv_state: Optional conversation state (not used by ThinkTool).
94
+ **params: Additional parameters (none supported).
95
+
96
+ Returns:
97
+ A sequence containing a single ThinkTool instance.
98
+
99
+ Raises:
100
+ ValueError: If any parameters are provided.
101
+ """
102
+ if params:
103
+ raise ValueError("ThinkTool doesn't accept parameters")
104
+ return [
105
+ cls(
106
+ description=THINK_DESCRIPTION,
107
+ action_type=ThinkAction,
108
+ observation_type=ThinkObservation,
109
+ executor=ThinkExecutor(),
110
+ annotations=ToolAnnotations(
111
+ readOnlyHint=True,
112
+ destructiveHint=False,
113
+ idempotentHint=True,
114
+ openWorldHint=False,
115
+ ),
116
+ )
117
+ ]