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,142 @@
1
+ import copy
2
+ from collections.abc import Sequence
3
+ from typing import ClassVar
4
+
5
+ from pydantic import ConfigDict, Field
6
+ from rich.text import Text
7
+
8
+ from openhands.sdk.event.base import N_CHAR_PREVIEW, EventID, LLMConvertibleEvent
9
+ from openhands.sdk.event.types import SourceType
10
+ from openhands.sdk.llm import (
11
+ ImageContent,
12
+ Message,
13
+ RedactedThinkingBlock,
14
+ TextContent,
15
+ ThinkingBlock,
16
+ content_to_str,
17
+ )
18
+
19
+
20
+ class MessageEvent(LLMConvertibleEvent):
21
+ """Message from either agent or user.
22
+
23
+ This is originally the "MessageAction", but it suppose not to be tool call."""
24
+
25
+ model_config: ClassVar[ConfigDict] = ConfigDict(extra="forbid", frozen=True)
26
+
27
+ source: SourceType
28
+ llm_message: Message = Field(
29
+ ..., description="The exact LLM message for this message event"
30
+ )
31
+ llm_response_id: EventID | None = Field(
32
+ default=None,
33
+ description=(
34
+ "Completion or Response ID of the LLM response that generated this event"
35
+ "If the source != 'agent', this field is None"
36
+ ),
37
+ )
38
+
39
+ # context extensions stuff / skill can go here
40
+ activated_skills: list[str] = Field(
41
+ default_factory=list, description="List of activated skill name"
42
+ )
43
+ extended_content: list[TextContent] = Field(
44
+ default_factory=list, description="List of content added by agent context"
45
+ )
46
+ sender: str | None = Field(
47
+ default=None,
48
+ description=(
49
+ "Optional identifier of the sender. "
50
+ "Can be used to track message origin in multi-agent scenarios."
51
+ ),
52
+ )
53
+
54
+ @property
55
+ def reasoning_content(self) -> str:
56
+ return self.llm_message.reasoning_content or ""
57
+
58
+ @property
59
+ def thinking_blocks(self) -> Sequence[ThinkingBlock | RedactedThinkingBlock]:
60
+ """Return the Anthropic thinking blocks from the LLM message."""
61
+ return self.llm_message.thinking_blocks
62
+
63
+ @property
64
+ def visualize(self) -> Text:
65
+ """Return Rich Text representation of this message event."""
66
+ content = Text()
67
+
68
+ # Message text content
69
+ text_parts = content_to_str(self.llm_message.content)
70
+ if text_parts:
71
+ full_content = "".join(text_parts)
72
+ content.append(full_content)
73
+ else:
74
+ content.append("[no text content]")
75
+
76
+ # Responses API reasoning (plaintext only; never render encrypted_content)
77
+ reasoning_item = self.llm_message.responses_reasoning_item
78
+ if reasoning_item is not None:
79
+ content.append("\n\nReasoning:\n", style="bold")
80
+ if reasoning_item.summary:
81
+ for s in reasoning_item.summary:
82
+ content.append(f"- {s}\n")
83
+ if reasoning_item.content:
84
+ for b in reasoning_item.content:
85
+ content.append(f"{b}\n")
86
+
87
+ # Add skill information if present
88
+ if self.activated_skills:
89
+ content.append(
90
+ f"\n\nActivated Skills: {', '.join(self.activated_skills)}",
91
+ )
92
+
93
+ # Add extended content if available
94
+ if self.extended_content:
95
+ assert not any(
96
+ isinstance(c, ImageContent) for c in self.extended_content
97
+ ), "Extended content should not contain images"
98
+ text_parts = content_to_str(self.extended_content)
99
+ content.append(
100
+ "\n\nPrompt Extension based on Agent Context:\n", style="bold"
101
+ )
102
+ content.append(" ".join(text_parts))
103
+
104
+ return content
105
+
106
+ def to_llm_message(self) -> Message:
107
+ msg = copy.deepcopy(self.llm_message)
108
+ msg.content = list(msg.content) + list(self.extended_content)
109
+ return msg
110
+
111
+ def __str__(self) -> str:
112
+ """Plain text string representation for MessageEvent."""
113
+ base_str = f"{self.__class__.__name__} ({self.source})"
114
+ # Extract text content from the message
115
+ text_parts = []
116
+ message = self.to_llm_message()
117
+ for content in message.content:
118
+ if isinstance(content, TextContent):
119
+ text_parts.append(content.text)
120
+ elif isinstance(content, ImageContent):
121
+ text_parts.append(f"[Image: {len(content.image_urls)} URLs]")
122
+
123
+ if text_parts:
124
+ content_preview = " ".join(text_parts)
125
+ if len(content_preview) > N_CHAR_PREVIEW:
126
+ content_preview = content_preview[: N_CHAR_PREVIEW - 3] + "..."
127
+ skill_info = (
128
+ f" [Skills: {', '.join(self.activated_skills)}]"
129
+ if self.activated_skills
130
+ else ""
131
+ )
132
+ thinking_info = (
133
+ f" [Thinking blocks: {len(self.thinking_blocks)}]"
134
+ if self.thinking_blocks
135
+ else ""
136
+ )
137
+ return (
138
+ f"{base_str}\n {message.role}: "
139
+ f"{content_preview}{skill_info}{thinking_info}"
140
+ )
141
+ else:
142
+ return f"{base_str}\n {message.role}: [no text content]"
@@ -0,0 +1,141 @@
1
+ from pydantic import Field
2
+ from rich.text import Text
3
+
4
+ from openhands.sdk.event.base import N_CHAR_PREVIEW, LLMConvertibleEvent
5
+ from openhands.sdk.event.types import EventID, SourceType, ToolCallID
6
+ from openhands.sdk.llm import Message, TextContent, content_to_str
7
+ from openhands.sdk.tool.schema import Observation
8
+
9
+
10
+ class ObservationBaseEvent(LLMConvertibleEvent):
11
+ """Base class for anything as a response to a tool call.
12
+
13
+ Examples include tool execution, error, user reject.
14
+ """
15
+
16
+ source: SourceType = "environment"
17
+ tool_name: str = Field(
18
+ ..., description="The tool name that this observation is responding to"
19
+ )
20
+ tool_call_id: ToolCallID = Field(
21
+ ..., description="The tool call id that this observation is responding to"
22
+ )
23
+
24
+
25
+ class ObservationEvent(ObservationBaseEvent):
26
+ observation: Observation = Field(
27
+ ..., description="The observation (tool call) sent to LLM"
28
+ )
29
+ action_id: EventID = Field(
30
+ ..., description="The action id that this observation is responding to"
31
+ )
32
+
33
+ @property
34
+ def visualize(self) -> Text:
35
+ """Return Rich Text representation of this observation event."""
36
+ to_viz = self.observation.visualize
37
+ content = Text()
38
+ if to_viz.plain.strip():
39
+ content.append("Tool: ", style="bold")
40
+ content.append(self.tool_name)
41
+ content.append("\nResult:\n", style="bold")
42
+ content.append(to_viz)
43
+ return content
44
+
45
+ def to_llm_message(self) -> Message:
46
+ return Message(
47
+ role="tool",
48
+ content=self.observation.to_llm_content,
49
+ name=self.tool_name,
50
+ tool_call_id=self.tool_call_id,
51
+ )
52
+
53
+ def __str__(self) -> str:
54
+ """Plain text string representation for ObservationEvent."""
55
+ base_str = f"{self.__class__.__name__} ({self.source})"
56
+ content_str = "".join(content_to_str(self.observation.to_llm_content))
57
+ obs_preview = (
58
+ content_str[:N_CHAR_PREVIEW] + "..."
59
+ if len(content_str) > N_CHAR_PREVIEW
60
+ else content_str
61
+ )
62
+ return f"{base_str}\n Tool: {self.tool_name}\n Result: {obs_preview}"
63
+
64
+
65
+ class UserRejectObservation(ObservationBaseEvent):
66
+ """Observation when user rejects an action in confirmation mode."""
67
+
68
+ rejection_reason: str = Field(
69
+ default="User rejected the action",
70
+ description="Reason for rejecting the action",
71
+ )
72
+ action_id: EventID = Field(
73
+ ..., description="The action id that this observation is responding to"
74
+ )
75
+
76
+ @property
77
+ def visualize(self) -> Text:
78
+ """Return Rich Text representation of this user rejection event."""
79
+ content = Text()
80
+ content.append("Tool: ", style="bold")
81
+ content.append(self.tool_name)
82
+ content.append("\n\nRejection Reason:\n", style="bold")
83
+ content.append(self.rejection_reason)
84
+ return content
85
+
86
+ def to_llm_message(self) -> Message:
87
+ return Message(
88
+ role="tool",
89
+ content=[TextContent(text=f"Action rejected: {self.rejection_reason}")],
90
+ name=self.tool_name,
91
+ tool_call_id=self.tool_call_id,
92
+ )
93
+
94
+ def __str__(self) -> str:
95
+ """Plain text string representation for UserRejectObservation."""
96
+ base_str = f"{self.__class__.__name__} ({self.source})"
97
+ reason_preview = (
98
+ self.rejection_reason[:N_CHAR_PREVIEW] + "..."
99
+ if len(self.rejection_reason) > N_CHAR_PREVIEW
100
+ else self.rejection_reason
101
+ )
102
+ return f"{base_str}\n Tool: {self.tool_name}\n Reason: {reason_preview}"
103
+
104
+
105
+ class AgentErrorEvent(ObservationBaseEvent):
106
+ """Error triggered by the agent.
107
+
108
+ Note: This event should not contain model "thought" or "reasoning_content". It
109
+ represents an error produced by the agent/scaffold, not model output.
110
+ """
111
+
112
+ source: SourceType = "agent"
113
+ error: str = Field(..., description="The error message from the scaffold")
114
+
115
+ @property
116
+ def visualize(self) -> Text:
117
+ """Return Rich Text representation of this agent error event."""
118
+ content = Text()
119
+ content.append("Error Details:\n", style="bold")
120
+ content.append(self.error)
121
+ return content
122
+
123
+ def to_llm_message(self) -> Message:
124
+ # Provide plain string error content; serializers handle Chat vs Responses.
125
+ # For Responses API, output is a string; JSON is not required.
126
+ return Message(
127
+ role="tool",
128
+ content=[TextContent(text=self.error)],
129
+ name=self.tool_name,
130
+ tool_call_id=self.tool_call_id,
131
+ )
132
+
133
+ def __str__(self) -> str:
134
+ """Plain text string representation for AgentErrorEvent."""
135
+ base_str = f"{self.__class__.__name__} ({self.source})"
136
+ error_preview = (
137
+ self.error[:N_CHAR_PREVIEW] + "..."
138
+ if len(self.error) > N_CHAR_PREVIEW
139
+ else self.error
140
+ )
141
+ return f"{base_str}\n Error: {error_preview}"
@@ -0,0 +1,61 @@
1
+ import json
2
+
3
+ from pydantic import Field
4
+ from rich.text import Text
5
+
6
+ from openhands.sdk.event.base import N_CHAR_PREVIEW, LLMConvertibleEvent
7
+ from openhands.sdk.event.types import SourceType
8
+ from openhands.sdk.llm import Message, TextContent
9
+ from openhands.sdk.tool import ToolDefinition
10
+
11
+
12
+ class SystemPromptEvent(LLMConvertibleEvent):
13
+ """System prompt added by the agent."""
14
+
15
+ source: SourceType = "agent"
16
+ system_prompt: TextContent = Field(..., description="The system prompt text")
17
+ tools: list[ToolDefinition] = Field(
18
+ ..., description="List of tools as ToolDefinition objects"
19
+ )
20
+
21
+ @property
22
+ def visualize(self) -> Text:
23
+ """Return Rich Text representation of this system prompt event."""
24
+ content = Text()
25
+ content.append("System Prompt:\n", style="bold")
26
+ content.append(self.system_prompt.text)
27
+ content.append(f"\n\nTools Available: {len(self.tools)}")
28
+ for tool in self.tools:
29
+ # Use ToolDefinition properties directly
30
+ description = tool.description.split("\n")[0][:100]
31
+ if len(description) < len(tool.description):
32
+ description += "..."
33
+
34
+ content.append(f"\n - {tool.name}: {description}\n")
35
+
36
+ # Get parameters from the action type schema
37
+ try:
38
+ params_dict = tool.action_type.to_mcp_schema()
39
+ params_str = json.dumps(params_dict)
40
+ if len(params_str) > 200:
41
+ params_str = params_str[:197] + "..."
42
+ content.append(f" Parameters: {params_str}")
43
+ except Exception:
44
+ content.append(" Parameters: <unavailable>")
45
+ return content
46
+
47
+ def to_llm_message(self) -> Message:
48
+ return Message(role="system", content=[self.system_prompt])
49
+
50
+ def __str__(self) -> str:
51
+ """Plain text string representation for SystemPromptEvent."""
52
+ base_str = f"{self.__class__.__name__} ({self.source})"
53
+ prompt_preview = (
54
+ self.system_prompt.text[:N_CHAR_PREVIEW] + "..."
55
+ if len(self.system_prompt.text) > N_CHAR_PREVIEW
56
+ else self.system_prompt.text
57
+ )
58
+ tool_count = len(self.tools)
59
+ return (
60
+ f"{base_str}\n System: {prompt_preview}\n Tools: {tool_count} available"
61
+ )
@@ -0,0 +1,16 @@
1
+ from pydantic import Field
2
+
3
+ from openhands.sdk.event.base import Event
4
+ from openhands.sdk.event.types import SourceType
5
+
6
+
7
+ class TokenEvent(Event):
8
+ """Event from VLLM representing token IDs used in LLM interaction."""
9
+
10
+ source: SourceType
11
+ prompt_token_ids: list[int] = Field(
12
+ ..., description="The exact prompt token IDs for this message event"
13
+ )
14
+ response_token_ids: list[int] = Field(
15
+ ..., description="The exact response token IDs for this message event"
16
+ )
@@ -0,0 +1,11 @@
1
+ from typing import Literal
2
+
3
+
4
+ EventType = Literal["action", "observation", "message", "system_prompt", "agent_error"]
5
+ SourceType = Literal["agent", "user", "environment"]
6
+
7
+ EventID = str
8
+ """Type alias for event IDs."""
9
+
10
+ ToolCallID = str
11
+ """Type alias for tool call IDs."""
@@ -0,0 +1,21 @@
1
+ from rich.text import Text
2
+
3
+ from openhands.sdk.event.base import Event
4
+ from openhands.sdk.event.types import SourceType
5
+
6
+
7
+ class PauseEvent(Event):
8
+ """Event indicating that the agent execution was paused by user request."""
9
+
10
+ source: SourceType = "user"
11
+
12
+ @property
13
+ def visualize(self) -> Text:
14
+ """Return Rich Text representation of this pause event."""
15
+ content = Text()
16
+ content.append("Conversation Paused", style="bold")
17
+ return content
18
+
19
+ def __str__(self) -> str:
20
+ """Plain text string representation for PauseEvent."""
21
+ return f"{self.__class__.__name__} ({self.source}): Agent execution paused"
@@ -0,0 +1,43 @@
1
+ """Git-related exceptions for OpenHands SDK."""
2
+
3
+
4
+ class GitError(Exception):
5
+ """Base exception for git-related errors."""
6
+
7
+ pass
8
+
9
+
10
+ class GitRepositoryError(GitError):
11
+ """Exception raised when git repository operations fail."""
12
+
13
+ command: str | None
14
+ exit_code: int | None
15
+
16
+ def __init__(
17
+ self, message: str, command: str | None = None, exit_code: int | None = None
18
+ ):
19
+ self.command = command
20
+ self.exit_code = exit_code
21
+ super().__init__(message)
22
+
23
+
24
+ class GitCommandError(GitError):
25
+ """Exception raised when git command execution fails."""
26
+
27
+ command: list[str]
28
+ exit_code: int
29
+ stderr: str
30
+
31
+ def __init__(
32
+ self, message: str, command: list[str], exit_code: int, stderr: str = ""
33
+ ):
34
+ self.command = command
35
+ self.exit_code = exit_code
36
+ self.stderr = stderr
37
+ super().__init__(message)
38
+
39
+
40
+ class GitPathError(GitError):
41
+ """Exception raised when git path operations fail."""
42
+
43
+ pass
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env python3
2
+ """Get git changes in the current working directory relative to the remote origin
3
+ if possible.
4
+ """
5
+
6
+ import glob
7
+ import json
8
+ import logging
9
+ import os
10
+ from pathlib import Path
11
+
12
+ from openhands.sdk.git.exceptions import GitCommandError
13
+ from openhands.sdk.git.models import GitChange, GitChangeStatus
14
+ from openhands.sdk.git.utils import (
15
+ get_valid_ref,
16
+ run_git_command,
17
+ validate_git_repository,
18
+ )
19
+
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ def _map_git_status_to_enum(status: str) -> GitChangeStatus:
25
+ """Map git status codes to GitChangeStatus enum values."""
26
+ status_mapping = {
27
+ "M": GitChangeStatus.UPDATED,
28
+ "A": GitChangeStatus.ADDED,
29
+ "D": GitChangeStatus.DELETED,
30
+ "U": GitChangeStatus.UPDATED, # Unmerged files are treated as updated
31
+ }
32
+ if status not in status_mapping:
33
+ raise ValueError(f"Unknown git status: {status}")
34
+ return status_mapping[status]
35
+
36
+
37
+ def get_changes_in_repo(repo_dir: str | Path) -> list[GitChange]:
38
+ """Get git changes in a repository relative to the origin default branch.
39
+
40
+ This is different from `git status` as it compares against the remote branch
41
+ rather than the staging area.
42
+
43
+ Args:
44
+ repo_dir: Path to the git repository
45
+
46
+ Returns:
47
+ List of GitChange objects representing the changes
48
+
49
+ Raises:
50
+ GitRepositoryError: If the directory is not a valid git repository
51
+ GitCommandError: If git commands fail
52
+ """
53
+ # Validate the repository first
54
+ validated_repo = validate_git_repository(repo_dir)
55
+
56
+ ref = get_valid_ref(validated_repo)
57
+ if not ref:
58
+ logger.warning(f"No valid git reference found for {validated_repo}")
59
+ return []
60
+
61
+ # Get changed files using secure git command
62
+ try:
63
+ changed_files_output = run_git_command(
64
+ ["git", "--no-pager", "diff", "--name-status", ref], validated_repo
65
+ )
66
+ changed_files = (
67
+ changed_files_output.splitlines() if changed_files_output else []
68
+ )
69
+ except GitCommandError as e:
70
+ logger.error(f"Failed to get git diff for {validated_repo}: {e}")
71
+ raise
72
+ changes = []
73
+ for line in changed_files:
74
+ if not line.strip():
75
+ logger.warning("Empty line in git diff output, skipping")
76
+ continue
77
+
78
+ # Handle different output formats from git diff --name-status
79
+ # Depending on git config, format can be either:
80
+ # * "A file.txt"
81
+ # * "A file.txt"
82
+ # * "R100 old_file.txt new_file.txt" (rename with similarity percentage)
83
+ parts = line.split()
84
+ if len(parts) < 2:
85
+ logger.error(f"Unexpected git diff line format: {line}")
86
+ raise GitCommandError(
87
+ message=f"Unexpected git diff output format: {line}",
88
+ command=["git", "diff", "--name-status"],
89
+ exit_code=0,
90
+ stderr="Invalid output format",
91
+ )
92
+
93
+ status = parts[0].strip()
94
+
95
+ # Handle rename operations (status starts with 'R' followed
96
+ # by similarity percentage)
97
+ if status.startswith("R") and len(parts) == 3:
98
+ # Rename: convert to delete (old path) + add (new path)
99
+ old_path = parts[1].strip()
100
+ new_path = parts[2].strip()
101
+ changes.append(
102
+ GitChange(
103
+ status=GitChangeStatus.DELETED,
104
+ path=Path(old_path),
105
+ )
106
+ )
107
+ changes.append(
108
+ GitChange(
109
+ status=GitChangeStatus.ADDED,
110
+ path=Path(new_path),
111
+ )
112
+ )
113
+ logger.debug(f"Found git rename: {old_path} -> {new_path}")
114
+ continue
115
+
116
+ # Handle copy operations (status starts with 'C' followed by
117
+ # similarity percentage)
118
+ elif status.startswith("C") and len(parts) == 3:
119
+ # Copy: only add the new path (original remains)
120
+ new_path = parts[2].strip()
121
+ changes.append(
122
+ GitChange(
123
+ status=GitChangeStatus.ADDED,
124
+ path=Path(new_path),
125
+ )
126
+ )
127
+ logger.debug(f"Found git copy: -> {new_path}")
128
+ continue
129
+
130
+ # Handle regular operations (M, A, D, etc.)
131
+ elif len(parts) == 2:
132
+ path = parts[1].strip()
133
+ else:
134
+ logger.error(f"Unexpected git diff line format: {line}")
135
+ raise GitCommandError(
136
+ message=f"Unexpected git diff output format: {line}",
137
+ command=["git", "diff", "--name-status"],
138
+ exit_code=0,
139
+ stderr="Invalid output format",
140
+ )
141
+
142
+ if status == "??":
143
+ status = "A"
144
+ elif status == "*":
145
+ status = "M"
146
+
147
+ # Check for valid single-character status codes
148
+ if status in {"M", "A", "D", "U"}:
149
+ try:
150
+ changes.append(
151
+ GitChange(
152
+ status=_map_git_status_to_enum(status),
153
+ path=Path(path),
154
+ )
155
+ )
156
+ logger.debug(f"Found git change: {status} {path}")
157
+ except ValueError as e:
158
+ logger.error(f"Unknown git status '{status}' for file {path}")
159
+ raise GitCommandError(
160
+ message=f"Unknown git status: {status}",
161
+ command=["git", "diff", "--name-status"],
162
+ exit_code=0,
163
+ stderr=f"Unknown status code: {status}",
164
+ ) from e
165
+ else:
166
+ logger.error(f"Unexpected git status '{status}' for file {path}")
167
+ raise GitCommandError(
168
+ message=f"Unexpected git status: {status}",
169
+ command=["git", "diff", "--name-status"],
170
+ exit_code=0,
171
+ stderr=f"Unexpected status code: {status}",
172
+ )
173
+
174
+ # Get untracked files
175
+ try:
176
+ untracked_output = run_git_command(
177
+ ["git", "--no-pager", "ls-files", "--others", "--exclude-standard"],
178
+ validated_repo,
179
+ )
180
+ untracked_files = untracked_output.splitlines() if untracked_output else []
181
+ except GitCommandError as e:
182
+ logger.error(f"Failed to get untracked files for {validated_repo}: {e}")
183
+ untracked_files = []
184
+ for path in untracked_files:
185
+ if path.strip():
186
+ changes.append(
187
+ GitChange(
188
+ status=GitChangeStatus.ADDED,
189
+ path=Path(path.strip()),
190
+ )
191
+ )
192
+ logger.debug(f"Found untracked file: {path}")
193
+
194
+ logger.info(f"Found {len(changes)} total git changes in {validated_repo}")
195
+ return changes
196
+
197
+
198
+ def get_git_changes(cwd: str | Path) -> list[GitChange]:
199
+ git_dirs = {
200
+ os.path.dirname(f)[2:]
201
+ for f in glob.glob("./*/.git", root_dir=cwd, recursive=True)
202
+ }
203
+
204
+ # First try the workspace directory
205
+ changes = get_changes_in_repo(cwd)
206
+
207
+ # Filter out any changes which are in one of the git directories
208
+ changes = [
209
+ change
210
+ for change in changes
211
+ if next(
212
+ iter(
213
+ git_dir for git_dir in git_dirs if str(change.path).startswith(git_dir)
214
+ ),
215
+ None,
216
+ )
217
+ is None
218
+ ]
219
+
220
+ # Add changes from git directories
221
+ for git_dir in git_dirs:
222
+ git_dir_changes = get_changes_in_repo(str(Path(cwd, git_dir)))
223
+ for change in git_dir_changes:
224
+ # Create a new GitChange with the updated path
225
+ updated_change = GitChange(
226
+ status=change.status,
227
+ path=Path(git_dir) / change.path,
228
+ )
229
+ changes.append(updated_change)
230
+
231
+ changes.sort(key=lambda change: str(change.path))
232
+
233
+ return changes
234
+
235
+
236
+ if __name__ == "__main__":
237
+ try:
238
+ changes = get_git_changes(os.getcwd())
239
+ # Convert GitChange objects to dictionaries for JSON serialization
240
+ changes_dict = [
241
+ {
242
+ "status": change.status.value,
243
+ "path": str(change.path),
244
+ }
245
+ for change in changes
246
+ ]
247
+ print(json.dumps(changes_dict))
248
+ except Exception as e:
249
+ print(json.dumps({"error": str(e)}))