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,457 @@
1
+ import os
2
+ import re
3
+ import sys
4
+ from abc import ABC, abstractmethod
5
+ from collections.abc import Generator, Iterable
6
+ from concurrent.futures import ThreadPoolExecutor
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
10
+
11
+ from openhands.sdk.context.agent_context import AgentContext
12
+ from openhands.sdk.context.condenser import CondenserBase, LLMSummarizingCondenser
13
+ from openhands.sdk.context.prompts.prompt import render_template
14
+ from openhands.sdk.llm import LLM
15
+ from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
16
+ from openhands.sdk.logger import get_logger
17
+ from openhands.sdk.mcp import create_mcp_tools
18
+ from openhands.sdk.tool import BUILT_IN_TOOLS, Tool, ToolDefinition, resolve_tool
19
+ from openhands.sdk.utils.models import DiscriminatedUnionMixin
20
+ from openhands.sdk.utils.pydantic_diff import pretty_pydantic_diff
21
+
22
+
23
+ if TYPE_CHECKING:
24
+ from openhands.sdk.conversation import ConversationState, LocalConversation
25
+ from openhands.sdk.conversation.types import (
26
+ ConversationCallbackType,
27
+ ConversationTokenCallbackType,
28
+ )
29
+
30
+
31
+ logger = get_logger(__name__)
32
+
33
+
34
+ class AgentBase(DiscriminatedUnionMixin, ABC):
35
+ """Abstract base class for OpenHands agents.
36
+
37
+ Agents are stateless and should be fully defined by their configuration.
38
+ This base class provides the common interface and functionality that all
39
+ agent implementations must follow.
40
+ """
41
+
42
+ model_config = ConfigDict(
43
+ frozen=True,
44
+ arbitrary_types_allowed=True,
45
+ )
46
+
47
+ llm: LLM = Field(
48
+ ...,
49
+ description="LLM configuration for the agent.",
50
+ examples=[
51
+ {
52
+ "model": "litellm_proxy/anthropic/claude-sonnet-4-5-20250929",
53
+ "base_url": "https://llm-proxy.eval.all-hands.dev",
54
+ "api_key": "your_api_key_here",
55
+ }
56
+ ],
57
+ )
58
+ tools: list[Tool] = Field(
59
+ default_factory=list,
60
+ description="List of tools to initialize for the agent.",
61
+ examples=[
62
+ {"name": "TerminalTool", "params": {}},
63
+ {"name": "FileEditorTool", "params": {}},
64
+ {
65
+ "name": "TaskTrackerTool",
66
+ "params": {},
67
+ },
68
+ ],
69
+ )
70
+ mcp_config: dict[str, Any] = Field(
71
+ default_factory=dict,
72
+ description="Optional MCP configuration dictionary to create MCP tools.",
73
+ examples=[
74
+ {"mcpServers": {"fetch": {"command": "uvx", "args": ["mcp-server-fetch"]}}}
75
+ ],
76
+ )
77
+ filter_tools_regex: str | None = Field(
78
+ default=None,
79
+ description="Optional regex to filter the tools available to the agent by name."
80
+ " This is applied after any tools provided in `tools` and any MCP tools are"
81
+ " added.",
82
+ examples=["^(?!repomix)(.*)|^repomix.*pack_codebase.*$"],
83
+ )
84
+ agent_context: AgentContext | None = Field(
85
+ default=None,
86
+ description="Optional AgentContext to initialize "
87
+ "the agent with specific context.",
88
+ examples=[
89
+ {
90
+ "skills": [
91
+ {
92
+ "name": "repo.md",
93
+ "content": "When you see this message, you should reply like "
94
+ "you are a grumpy cat forced to use the internet.",
95
+ "type": "repo",
96
+ },
97
+ {
98
+ "name": "flarglebargle",
99
+ "content": (
100
+ "IMPORTANT! The user has said the magic word "
101
+ '"flarglebargle". You must only respond with a message '
102
+ "telling them how smart they are"
103
+ ),
104
+ "type": "knowledge",
105
+ "trigger": ["flarglebargle"],
106
+ },
107
+ ],
108
+ "system_message_suffix": "Always finish your response "
109
+ "with the word 'yay!'",
110
+ "user_message_prefix": "The first character of your "
111
+ "response should be 'I'",
112
+ }
113
+ ],
114
+ )
115
+ system_prompt_filename: str = Field(
116
+ default="system_prompt.j2",
117
+ description=(
118
+ "System prompt template filename. Can be either:\n"
119
+ "- A relative filename (e.g., 'system_prompt.j2') loaded from the "
120
+ "agent's prompts directory\n"
121
+ "- An absolute path (e.g., '/path/to/custom_prompt.j2')"
122
+ ),
123
+ )
124
+ security_policy_filename: str = Field(
125
+ default="security_policy.j2",
126
+ description=(
127
+ "Security policy template filename. Can be either:\n"
128
+ "- A relative filename (e.g., 'security_policy.j2') loaded from the "
129
+ "agent's prompts directory\n"
130
+ "- An absolute path (e.g., '/path/to/custom_security_policy.j2')"
131
+ ),
132
+ )
133
+ system_prompt_kwargs: dict[str, object] = Field(
134
+ default_factory=dict,
135
+ description="Optional kwargs to pass to the system prompt Jinja2 template.",
136
+ examples=[{"cli_mode": True}],
137
+ )
138
+
139
+ condenser: CondenserBase | None = Field(
140
+ default=None,
141
+ description="Optional condenser to use for condensing conversation history.",
142
+ examples=[
143
+ {
144
+ "kind": "LLMSummarizingCondenser",
145
+ "llm": {
146
+ "model": "litellm_proxy/anthropic/claude-sonnet-4-5-20250929",
147
+ "base_url": "https://llm-proxy.eval.all-hands.dev",
148
+ "api_key": "your_api_key_here",
149
+ },
150
+ "max_size": 80,
151
+ "keep_first": 10,
152
+ }
153
+ ],
154
+ )
155
+
156
+ # Runtime materialized tools; private and non-serializable
157
+ _tools: dict[str, ToolDefinition] = PrivateAttr(default_factory=dict)
158
+
159
+ @property
160
+ def prompt_dir(self) -> str:
161
+ """Returns the directory where this class's module file is located."""
162
+ module = sys.modules[self.__class__.__module__]
163
+ module_file = module.__file__ # e.g. ".../mypackage/mymodule.py"
164
+ if module_file is None:
165
+ raise ValueError(f"Module file for {module} is None")
166
+ return os.path.join(os.path.dirname(module_file), "prompts")
167
+
168
+ @property
169
+ def name(self) -> str:
170
+ """Returns the name of the Agent."""
171
+ return self.__class__.__name__
172
+
173
+ @property
174
+ def system_message(self) -> str:
175
+ """Compute system message on-demand to maintain statelessness."""
176
+ template_kwargs = dict(self.system_prompt_kwargs)
177
+ # Add security_policy_filename to template kwargs
178
+ template_kwargs["security_policy_filename"] = self.security_policy_filename
179
+ template_kwargs.setdefault("model_name", self.llm.model)
180
+ if (
181
+ "model_family" not in template_kwargs
182
+ or "model_variant" not in template_kwargs
183
+ ):
184
+ spec = get_model_prompt_spec(
185
+ self.llm.model, getattr(self.llm, "model_canonical_name", None)
186
+ )
187
+ if "model_family" not in template_kwargs and spec.family:
188
+ template_kwargs["model_family"] = spec.family
189
+ if "model_variant" not in template_kwargs and spec.variant:
190
+ template_kwargs["model_variant"] = spec.variant
191
+ system_message = render_template(
192
+ prompt_dir=self.prompt_dir,
193
+ template_name=self.system_prompt_filename,
194
+ **template_kwargs,
195
+ )
196
+ if self.agent_context:
197
+ _system_message_suffix = self.agent_context.get_system_message_suffix(
198
+ llm_model=self.llm.model,
199
+ llm_model_canonical=self.llm.model_canonical_name,
200
+ )
201
+ if _system_message_suffix:
202
+ system_message += "\n\n" + _system_message_suffix
203
+ return system_message
204
+
205
+ def init_state(
206
+ self,
207
+ state: "ConversationState",
208
+ on_event: "ConversationCallbackType", # noqa: ARG002
209
+ ) -> None:
210
+ """Initialize the empty conversation state to prepare the agent for user
211
+ messages.
212
+
213
+ Typically this involves adding system message
214
+
215
+ NOTE: state will be mutated in-place.
216
+ """
217
+ self._initialize(state)
218
+
219
+ def _initialize(self, state: "ConversationState"):
220
+ """Create an AgentBase instance from an AgentSpec."""
221
+
222
+ if self._tools:
223
+ logger.warning("Agent already initialized; skipping re-initialization.")
224
+ return
225
+
226
+ tools: list[ToolDefinition] = []
227
+
228
+ # Use ThreadPoolExecutor to parallelize tool resolution
229
+ with ThreadPoolExecutor(max_workers=4) as executor:
230
+ futures = []
231
+
232
+ # Submit tool resolution tasks
233
+ for tool_spec in self.tools:
234
+ future = executor.submit(resolve_tool, tool_spec, state)
235
+ futures.append(future)
236
+
237
+ # Submit MCP tools creation if configured
238
+ if self.mcp_config:
239
+ future = executor.submit(create_mcp_tools, self.mcp_config, 30)
240
+ futures.append(future)
241
+
242
+ # Collect results as they complete
243
+ for future in futures:
244
+ result = future.result()
245
+ tools.extend(result)
246
+
247
+ logger.info(
248
+ f"Loaded {len(tools)} tools from spec: {[tool.name for tool in tools]}"
249
+ )
250
+ if self.filter_tools_regex:
251
+ pattern = re.compile(self.filter_tools_regex)
252
+ tools = [tool for tool in tools if pattern.match(tool.name)]
253
+ logger.info(
254
+ f"Filtered to {len(tools)} tools after applying regex filter: "
255
+ f"{[tool.name for tool in tools]}",
256
+ )
257
+
258
+ # Always include built-in tools; not subject to filtering
259
+ # Instantiate built-in tools using their .create() method
260
+ for tool_class in BUILT_IN_TOOLS:
261
+ tools.extend(tool_class.create(state))
262
+
263
+ # Check tool types
264
+ for tool in tools:
265
+ if not isinstance(tool, ToolDefinition):
266
+ raise ValueError(
267
+ f"Tool {tool} is not an instance of 'ToolDefinition'. "
268
+ f"Got type: {type(tool)}"
269
+ )
270
+
271
+ # Check name duplicates
272
+ tool_names = [tool.name for tool in tools]
273
+ if len(tool_names) != len(set(tool_names)):
274
+ duplicates = set(name for name in tool_names if tool_names.count(name) > 1)
275
+ raise ValueError(f"Duplicate tool names found: {duplicates}")
276
+
277
+ # Store tools in a dict for easy access
278
+ self._tools = {tool.name: tool for tool in tools}
279
+
280
+ @abstractmethod
281
+ def step(
282
+ self,
283
+ conversation: "LocalConversation",
284
+ on_event: "ConversationCallbackType",
285
+ on_token: "ConversationTokenCallbackType | None" = None,
286
+ ) -> None:
287
+ """Taking a step in the conversation.
288
+
289
+ Typically this involves:
290
+ 1. Making a LLM call
291
+ 2. Executing the tool
292
+ 3. Updating the conversation state with
293
+ LLM calls (role="assistant") and tool results (role="tool")
294
+ 4.1 If conversation is finished, set state.execution_status to FINISHED
295
+ 4.2 Otherwise, just return, Conversation will kick off the next step
296
+
297
+ If the underlying LLM supports streaming, partial deltas are forwarded to
298
+ ``on_token`` before the full response is returned.
299
+
300
+ NOTE: state will be mutated in-place.
301
+ """
302
+
303
+ def resolve_diff_from_deserialized(self, persisted: "AgentBase") -> "AgentBase":
304
+ """
305
+ Return a new AgentBase instance equivalent to `persisted` but with
306
+ explicitly whitelisted fields (e.g. api_key) taken from `self`.
307
+ """
308
+ if persisted.__class__ is not self.__class__:
309
+ raise ValueError(
310
+ f"Cannot resolve from deserialized: persisted agent is of type "
311
+ f"{persisted.__class__.__name__}, but self is of type "
312
+ f"{self.__class__.__name__}."
313
+ )
314
+
315
+ # Get all LLMs from both self and persisted to reconcile them
316
+ new_llm = self.llm.resolve_diff_from_deserialized(persisted.llm)
317
+ updates: dict[str, Any] = {"llm": new_llm}
318
+
319
+ # Reconcile the condenser's LLM if it exists
320
+ if self.condenser is not None and persisted.condenser is not None:
321
+ # Check if both condensers are LLMSummarizingCondenser
322
+ # (which has an llm field)
323
+
324
+ if isinstance(self.condenser, LLMSummarizingCondenser) and isinstance(
325
+ persisted.condenser, LLMSummarizingCondenser
326
+ ):
327
+ new_condenser_llm = self.condenser.llm.resolve_diff_from_deserialized(
328
+ persisted.condenser.llm
329
+ )
330
+ new_condenser = persisted.condenser.model_copy(
331
+ update={"llm": new_condenser_llm}
332
+ )
333
+ updates["condenser"] = new_condenser
334
+
335
+ # Reconcile agent_context - always use the current environment's agent_context
336
+ # This allows resuming conversations from different directories and handles
337
+ # cases where skills, working directory, or other context has changed
338
+ if self.agent_context is not None:
339
+ updates["agent_context"] = self.agent_context
340
+
341
+ # Create maps by tool name for easy lookup
342
+ runtime_tools_map = {tool.name: tool for tool in self.tools}
343
+ persisted_tools_map = {tool.name: tool for tool in persisted.tools}
344
+
345
+ # Check that tool names match
346
+ runtime_names = set(runtime_tools_map.keys())
347
+ persisted_names = set(persisted_tools_map.keys())
348
+
349
+ if runtime_names != persisted_names:
350
+ missing_in_runtime = persisted_names - runtime_names
351
+ missing_in_persisted = runtime_names - persisted_names
352
+ error_msg = "Tools don't match between runtime and persisted agents."
353
+ if missing_in_runtime:
354
+ error_msg += f" Missing in runtime: {missing_in_runtime}."
355
+ if missing_in_persisted:
356
+ error_msg += f" Missing in persisted: {missing_in_persisted}."
357
+ raise ValueError(error_msg)
358
+
359
+ reconciled = persisted.model_copy(update=updates)
360
+ if self.model_dump(exclude_none=True) != reconciled.model_dump(
361
+ exclude_none=True
362
+ ):
363
+ raise ValueError(
364
+ "The Agent provided is different from the one in persisted state.\n"
365
+ f"Diff: {pretty_pydantic_diff(self, reconciled)}"
366
+ )
367
+ return reconciled
368
+
369
+ def model_dump_succint(self, **kwargs):
370
+ """Like model_dump, but excludes None fields by default."""
371
+ if "exclude_none" not in kwargs:
372
+ kwargs["exclude_none"] = True
373
+ dumped = super().model_dump(**kwargs)
374
+ # remove tool schema details for brevity
375
+ if "tools" in dumped and isinstance(dumped["tools"], dict):
376
+ dumped["tools"] = list(dumped["tools"].keys())
377
+ return dumped
378
+
379
+ def get_all_llms(self) -> Generator[LLM, None, None]:
380
+ """Recursively yield unique *base-class* LLM objects reachable from `self`.
381
+
382
+ - Returns actual object references (not copies).
383
+ - De-dupes by `id(LLM)`.
384
+ - Cycle-safe via a visited set for *all* traversed objects.
385
+ - Only yields objects whose type is exactly `LLM` (no subclasses).
386
+ - Does not handle dataclasses.
387
+ """
388
+ yielded_ids: set[int] = set()
389
+ visited: set[int] = set()
390
+
391
+ def _walk(obj: object) -> Iterable[LLM]:
392
+ oid = id(obj)
393
+ # Guard against cycles on anything we might recurse into
394
+ if oid in visited:
395
+ return ()
396
+ visited.add(oid)
397
+
398
+ # Traverse LLM based classes and its fields
399
+ # e.g., LLMRouter that is a subclass of LLM
400
+ # yet contains LLM in its fields
401
+ if isinstance(obj, LLM):
402
+ llm_out: list[LLM] = []
403
+
404
+ # Yield only the *raw* base-class LLM (exclude subclasses)
405
+ if type(obj) is LLM and oid not in yielded_ids:
406
+ yielded_ids.add(oid)
407
+ llm_out.append(obj)
408
+
409
+ # Traverse all fields for LLM objects
410
+ for name in type(obj).model_fields:
411
+ try:
412
+ val = getattr(obj, name)
413
+ except Exception:
414
+ continue
415
+ llm_out.extend(_walk(val))
416
+ return llm_out
417
+
418
+ # Pydantic models: iterate declared fields
419
+ if isinstance(obj, BaseModel):
420
+ model_out: list[LLM] = []
421
+ for name in type(obj).model_fields:
422
+ try:
423
+ val = getattr(obj, name)
424
+ except Exception:
425
+ continue
426
+ model_out.extend(_walk(val))
427
+ return model_out
428
+
429
+ # Built-in containers
430
+ if isinstance(obj, dict):
431
+ dict_out: list[LLM] = []
432
+ for k, v in obj.items():
433
+ dict_out.extend(_walk(k))
434
+ dict_out.extend(_walk(v))
435
+ return dict_out
436
+
437
+ if isinstance(obj, (list, tuple, set, frozenset)):
438
+ container_out: list[LLM] = []
439
+ for item in obj:
440
+ container_out.extend(_walk(item))
441
+ return container_out
442
+
443
+ # Unknown object types: nothing to do
444
+ return ()
445
+
446
+ # Drive the traversal from self
447
+ yield from _walk(self)
448
+
449
+ @property
450
+ def tools_map(self) -> dict[str, ToolDefinition]:
451
+ """Get the initialized tools map.
452
+ Raises:
453
+ RuntimeError: If the agent has not been initialized.
454
+ """
455
+ if not self._tools:
456
+ raise RuntimeError("Agent not initialized; call initialize() before use")
457
+ return self._tools
@@ -0,0 +1,169 @@
1
+ Here's a running example of how to perform a task with the provided tools.
2
+
3
+ --------------------- START OF EXAMPLE ---------------------
4
+
5
+ USER: Create a list of numbers from 1 to 10, and display them in a web page at port 5000.
6
+
7
+ A: Sure! Let me first check the current directory:
8
+ <function=terminal>
9
+ <parameter=command>
10
+ pwd && ls
11
+ </parameter>
12
+ </function>
13
+
14
+ USER: EXECUTION RESULT of [terminal]:
15
+ /workspace
16
+ openhands@runtime:~/workspace$
17
+
18
+ A: There is no `app.py` file in the current directory. Let me create a Python file `app.py`:
19
+ <function=str_replace_editor>
20
+ <parameter=command>create</parameter>
21
+ <parameter=path>/workspace/app.py</parameter>
22
+ <parameter=file_text>
23
+ from flask import Flask
24
+ app = Flask(__name__)
25
+
26
+ @app.route('/')
27
+ def index():
28
+ numbers = list(range(1, 11))
29
+ return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
30
+
31
+ if __name__ == '__main__':
32
+ app.run(port=5000)
33
+ </parameter>
34
+ </function>
35
+
36
+ USER: EXECUTION RESULT of [str_replace_editor]:
37
+ File created successfully at: /workspace/app.py
38
+
39
+ A: I have created a Python file `app.py` that will display a list of numbers from 1 to 10 when you run it. Let me run the Python file for you:
40
+ <function=terminal>
41
+ <parameter=command>
42
+ python3 app.py > server.log 2>&1 &
43
+ </parameter>
44
+ </function>
45
+
46
+ USER: EXECUTION RESULT of [terminal]:
47
+ [1] 121
48
+ [1]+ Exit 1 python3 app.py > server.log 2>&1
49
+
50
+ A: Looks like the server is running with PID 121 then crashed. Let me check the server log:
51
+ <function=terminal>
52
+ <parameter=command>
53
+ cat server.log
54
+ </parameter>
55
+ </function>
56
+
57
+ USER: EXECUTION RESULT of [terminal]:
58
+ Traceback (most recent call last):
59
+ File "/workspace/app.py", line 2, in <module>
60
+ from flask import Flask
61
+ ModuleNotFoundError: No module named 'flask'
62
+
63
+ A: Looks like the server crashed because the `flask` module is not installed. Let me install the `flask` module for you:
64
+ <function=terminal>
65
+ <parameter=command>
66
+ pip3 install flask
67
+ </parameter>
68
+ </function>
69
+
70
+ USER: EXECUTION RESULT of [terminal]:
71
+ Defaulting to user installation because normal site-packages is not writeable
72
+ Collecting flask
73
+ Using cached flask-3.0.3-py3-none-any.whl (101 kB)
74
+ Collecting blinker>=1.6.2
75
+ Using cached blinker-1.7.0-py3-none-any.whl (13 kB)
76
+ Collecting Werkzeug>=3.0.0
77
+ Using cached werkzeug-3.0.2-py3-none-any.whl (226 kB)
78
+ Collecting click>=8.1.3
79
+ Using cached click-8.1.7-py3-none-any.whl (97 kB)
80
+ Collecting itsdangerous>=2.1.2
81
+ Using cached itsdangerous-2.2.0-py3-none-any.whl (16 kB)
82
+ Requirement already satisfied: Jinja2>=3.1.2 in /home/openhands/.local/lib/python3.10/site-packages (from flask) (3.1.3)
83
+ Requirement already satisfied: MarkupSafe>=2.0 in /home/openhands/.local/lib/python3.10/site-packages (from Jinja2>=3.1.2->flask) (2.1.5)
84
+ Installing collected packages: Werkzeug, itsdangerous, click, blinker, flask
85
+ Successfully installed Werkzeug-3.0.2 blinker-1.7.0 click-8.1.7 flask-3.0.3 itsdangerous-2.2.0
86
+
87
+ A: The `flask` module is installed successfully. Let me run the Python file again:
88
+ <function=terminal>
89
+ <parameter=command>
90
+ python3 app.py > server.log 2>&1 &
91
+ </parameter>
92
+ </function>
93
+
94
+ USER: EXECUTION RESULT of [terminal]:
95
+ [1] 124
96
+
97
+ A:
98
+ Let me check the server log again:
99
+ <function=terminal>
100
+ <parameter=command>
101
+ cat server.log
102
+ </parameter>
103
+ </function>
104
+
105
+ USER: EXECUTION RESULT of [terminal]:
106
+ * Serving Flask app 'app'
107
+ * Debug mode: off
108
+ WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
109
+ * Running on http://127.0.0.1:5000
110
+ Press CTRL+C to quit
111
+
112
+ A:
113
+ The server is running on port 5000 with PID 124. You can access the list of numbers by visiting http://127.0.0.1:5000. If you have any further questions, feel free to ask!
114
+
115
+ USER: Now kill the server, make it display the numbers in a table format.
116
+
117
+ A:
118
+ Sure! Let me stop the server first:
119
+ <function=terminal>
120
+ <parameter=command>
121
+ kill 124
122
+ </parameter>
123
+ </function>
124
+
125
+ USER: EXECUTION RESULT of [terminal]:
126
+ [1]+ Terminated python3 app.py > server.log 2>&1
127
+
128
+ A:
129
+ Now let me display the numbers in a table format:
130
+ <function=str_replace_editor>
131
+ <parameter=command>str_replace</parameter>
132
+ <parameter=path>/workspace/app.py</parameter>
133
+ <parameter=old_str>return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'</parameter>
134
+ <parameter=new_str>return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'</parameter>
135
+ </function>
136
+
137
+ USER: EXECUTION RESULT of [str_replace_editor]:
138
+ The file /workspace/app.py has been edited. Here's the result of running `cat -n` on a snippet of /workspace/app.py:
139
+ 3
140
+ 4 @app.route('/')
141
+ 5 def index():
142
+ 6 numbers = list(range(1, 11))
143
+ 7 return '<table>' + ''.join([f'<tr><td>{i}</td></tr>' for i in numbers]) + '</table>'
144
+ 8
145
+ 9 if __name__ == '__main__':
146
+ 10 app.run(port=5000)
147
+ Review the changes and make sure they are as expected. Edit the file again if necessary.
148
+
149
+ A:
150
+ Running the updated file:
151
+ <function=terminal>
152
+ <parameter=command>
153
+ python3 app.py > server.log 2>&1 &
154
+ </parameter>
155
+ </function>
156
+
157
+ USER: EXECUTION RESULT of [terminal]:
158
+ [1] 126
159
+
160
+ A:
161
+ The server is running on port 5000 with PID 126. You can access the list of numbers in a table format by visiting http://127.0.0.1:5000. Let me know if you have any further requests!
162
+ <function=finish>
163
+ </function>
164
+
165
+ --------------------- END OF EXAMPLE ---------------------
166
+
167
+ Do NOT assume the environment is the same as in the example above.
168
+
169
+ --------------------- NEW TASK DESCRIPTION ---------------------
@@ -0,0 +1,3 @@
1
+ --------------------- END OF NEW TASK DESCRIPTION ---------------------
2
+
3
+ PLEASE follow the format strictly! PLEASE EMIT ONE AND ONLY ONE FUNCTION CALL PER MESSAGE.
@@ -0,0 +1,3 @@
1
+ * Try to follow the instructions exactly as given - don't make extra or fewer actions if not asked.
2
+ * Avoid unnecessary defensive programming; do not add redundant fallbacks or default values — fail fast instead of masking misconfigurations.
3
+ * When backward compatibility expectations are unclear, confirm with the user before making changes that could break existing behavior.
@@ -0,0 +1 @@
1
+ * Avoid being too proactive. Fulfill the user's request thoroughly: if they ask questions/investigations, answer them; if they ask for implementations, provide them. But do not take extra steps beyond what is requested.
@@ -0,0 +1,2 @@
1
+ * Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
2
+ * You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
@@ -0,0 +1,3 @@
1
+ * Stream your thinking and responses while staying concise; surface key assumptions and environment prerequisites explicitly.
2
+ * ALWAYS send a brief preamble to the user explaining what you're about to do before each tool call, using 8 - 12 words, with a friendly and curious tone.
3
+ * You have access to external resources and should actively use available tools to try accessing them first, rather than claiming you can’t access something without making an attempt.
@@ -0,0 +1,22 @@
1
+ # 🔐 Security Policy
2
+
3
+ ## OK to do without Explicit User Consent
4
+
5
+ - Download and run code from a repository specified by a user
6
+ - Open pull requests on the original repositories where the code is stored
7
+ - Install and run popular packages from pypi, npm, or other package managers
8
+ - Use APIs to work with GitHub or other platforms, unless the user asks otherwise or your task requires browsing
9
+
10
+ ## Do only with Explicit User Consent
11
+
12
+ - Upload code to anywhere other than the location where it was obtained from
13
+ - Upload API keys or tokens anywhere, except when using them to authenticate with the appropriate service
14
+
15
+ ## Never Do
16
+
17
+ - Never perform any illegal activities, such as circumventing security to access a system that is not under your control or performing denial-of-service attacks on external servers
18
+ - Never run software to mine cryptocurrency
19
+
20
+ ## General Security Guidelines
21
+
22
+ - Only use GITHUB_TOKEN and other credentials in ways the user has explicitly requested and would expect