openhands-sdk 1.7.3__py3-none-any.whl → 1.8.0__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.
- openhands/sdk/__init__.py +2 -0
- openhands/sdk/agent/agent.py +31 -1
- openhands/sdk/agent/base.py +111 -67
- openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
- openhands/sdk/agent/utils.py +3 -0
- openhands/sdk/context/agent_context.py +45 -3
- openhands/sdk/context/condenser/__init__.py +2 -0
- openhands/sdk/context/condenser/base.py +59 -8
- openhands/sdk/context/condenser/llm_summarizing_condenser.py +38 -10
- openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +4 -0
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +9 -0
- openhands/sdk/context/skills/__init__.py +12 -0
- openhands/sdk/context/skills/skill.py +425 -228
- openhands/sdk/context/skills/types.py +4 -0
- openhands/sdk/context/skills/utils.py +442 -0
- openhands/sdk/context/view.py +2 -0
- openhands/sdk/conversation/impl/local_conversation.py +42 -14
- openhands/sdk/conversation/impl/remote_conversation.py +99 -55
- openhands/sdk/conversation/state.py +54 -18
- openhands/sdk/event/llm_convertible/action.py +20 -0
- openhands/sdk/git/utils.py +31 -6
- openhands/sdk/hooks/conversation_hooks.py +57 -10
- openhands/sdk/llm/llm.py +59 -76
- openhands/sdk/llm/options/chat_options.py +4 -1
- openhands/sdk/llm/router/base.py +12 -0
- openhands/sdk/llm/utils/telemetry.py +2 -2
- openhands/sdk/llm/utils/verified_models.py +1 -1
- openhands/sdk/mcp/tool.py +3 -1
- openhands/sdk/plugin/__init__.py +22 -0
- openhands/sdk/plugin/plugin.py +299 -0
- openhands/sdk/plugin/types.py +226 -0
- openhands/sdk/tool/__init__.py +7 -1
- openhands/sdk/tool/builtins/__init__.py +4 -0
- openhands/sdk/tool/schema.py +6 -3
- openhands/sdk/tool/tool.py +60 -9
- openhands/sdk/utils/models.py +198 -472
- openhands/sdk/workspace/base.py +22 -0
- openhands/sdk/workspace/local.py +16 -0
- openhands/sdk/workspace/remote/async_remote_workspace.py +16 -0
- openhands/sdk/workspace/remote/base.py +16 -0
- {openhands_sdk-1.7.3.dist-info → openhands_sdk-1.8.0.dist-info}/METADATA +2 -2
- {openhands_sdk-1.7.3.dist-info → openhands_sdk-1.8.0.dist-info}/RECORD +44 -40
- {openhands_sdk-1.7.3.dist-info → openhands_sdk-1.8.0.dist-info}/WHEEL +0 -0
- {openhands_sdk-1.7.3.dist-info → openhands_sdk-1.8.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Type definitions for Plugin module."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import frontmatter
|
|
10
|
+
from pydantic import BaseModel, Field
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PluginAuthor(BaseModel):
|
|
14
|
+
"""Author information for a plugin."""
|
|
15
|
+
|
|
16
|
+
name: str = Field(description="Author's name")
|
|
17
|
+
email: str | None = Field(default=None, description="Author's email address")
|
|
18
|
+
|
|
19
|
+
@classmethod
|
|
20
|
+
def from_string(cls, author_str: str) -> PluginAuthor:
|
|
21
|
+
"""Parse author from string format 'Name <email>'."""
|
|
22
|
+
if "<" in author_str and ">" in author_str:
|
|
23
|
+
name = author_str.split("<")[0].strip()
|
|
24
|
+
email = author_str.split("<")[1].split(">")[0].strip()
|
|
25
|
+
return cls(name=name, email=email)
|
|
26
|
+
return cls(name=author_str.strip())
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class PluginManifest(BaseModel):
|
|
30
|
+
"""Plugin manifest from plugin.json."""
|
|
31
|
+
|
|
32
|
+
name: str = Field(description="Plugin name")
|
|
33
|
+
version: str = Field(default="1.0.0", description="Plugin version")
|
|
34
|
+
description: str = Field(default="", description="Plugin description")
|
|
35
|
+
author: PluginAuthor | None = Field(default=None, description="Plugin author")
|
|
36
|
+
|
|
37
|
+
model_config = {"extra": "allow"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _extract_examples(description: str) -> list[str]:
|
|
41
|
+
"""Extract <example> tags from description for agent triggering."""
|
|
42
|
+
pattern = r"<example>(.*?)</example>"
|
|
43
|
+
matches = re.findall(pattern, description, re.DOTALL | re.IGNORECASE)
|
|
44
|
+
return [m.strip() for m in matches if m.strip()]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class AgentDefinition(BaseModel):
|
|
48
|
+
"""Agent definition loaded from markdown file.
|
|
49
|
+
|
|
50
|
+
Agents are specialized configurations that can be triggered based on
|
|
51
|
+
user input patterns. They define custom system prompts and tool access.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
name: str = Field(description="Agent name (from frontmatter or filename)")
|
|
55
|
+
description: str = Field(default="", description="Agent description")
|
|
56
|
+
model: str = Field(
|
|
57
|
+
default="inherit", description="Model to use ('inherit' uses parent model)"
|
|
58
|
+
)
|
|
59
|
+
color: str | None = Field(default=None, description="Display color for the agent")
|
|
60
|
+
tools: list[str] = Field(
|
|
61
|
+
default_factory=list, description="List of allowed tools for this agent"
|
|
62
|
+
)
|
|
63
|
+
system_prompt: str = Field(default="", description="System prompt content")
|
|
64
|
+
source: str | None = Field(
|
|
65
|
+
default=None, description="Source file path for this agent"
|
|
66
|
+
)
|
|
67
|
+
# whenToUse examples extracted from description
|
|
68
|
+
when_to_use_examples: list[str] = Field(
|
|
69
|
+
default_factory=list,
|
|
70
|
+
description="Examples of when to use this agent (for triggering)",
|
|
71
|
+
)
|
|
72
|
+
# Raw frontmatter for any additional fields
|
|
73
|
+
metadata: dict[str, Any] = Field(
|
|
74
|
+
default_factory=dict, description="Additional metadata from frontmatter"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def load(cls, agent_path: Path) -> AgentDefinition:
|
|
79
|
+
"""Load an agent definition from a markdown file.
|
|
80
|
+
|
|
81
|
+
Agent markdown files have YAML frontmatter with:
|
|
82
|
+
- name: Agent name
|
|
83
|
+
- description: Description with optional <example> tags for triggering
|
|
84
|
+
- model: Model to use (default: 'inherit')
|
|
85
|
+
- color: Display color
|
|
86
|
+
- tools: List of allowed tools
|
|
87
|
+
|
|
88
|
+
The body of the markdown is the system prompt.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
agent_path: Path to the agent markdown file.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Loaded AgentDefinition instance.
|
|
95
|
+
"""
|
|
96
|
+
with open(agent_path) as f:
|
|
97
|
+
post = frontmatter.load(f)
|
|
98
|
+
|
|
99
|
+
fm = post.metadata
|
|
100
|
+
content = post.content.strip()
|
|
101
|
+
|
|
102
|
+
# Extract frontmatter fields with proper type handling
|
|
103
|
+
name = str(fm.get("name", agent_path.stem))
|
|
104
|
+
description = str(fm.get("description", ""))
|
|
105
|
+
model = str(fm.get("model", "inherit"))
|
|
106
|
+
color_raw = fm.get("color")
|
|
107
|
+
color: str | None = str(color_raw) if color_raw is not None else None
|
|
108
|
+
tools_raw = fm.get("tools", [])
|
|
109
|
+
|
|
110
|
+
# Ensure tools is a list of strings
|
|
111
|
+
tools: list[str]
|
|
112
|
+
if isinstance(tools_raw, str):
|
|
113
|
+
tools = [tools_raw]
|
|
114
|
+
elif isinstance(tools_raw, list):
|
|
115
|
+
tools = [str(t) for t in tools_raw]
|
|
116
|
+
else:
|
|
117
|
+
tools = []
|
|
118
|
+
|
|
119
|
+
# Extract whenToUse examples from description
|
|
120
|
+
when_to_use_examples = _extract_examples(description)
|
|
121
|
+
|
|
122
|
+
# Remove known fields from metadata to get extras
|
|
123
|
+
known_fields = {"name", "description", "model", "color", "tools"}
|
|
124
|
+
metadata = {k: v for k, v in fm.items() if k not in known_fields}
|
|
125
|
+
|
|
126
|
+
return cls(
|
|
127
|
+
name=name,
|
|
128
|
+
description=description,
|
|
129
|
+
model=model,
|
|
130
|
+
color=color,
|
|
131
|
+
tools=tools,
|
|
132
|
+
system_prompt=content,
|
|
133
|
+
source=str(agent_path),
|
|
134
|
+
when_to_use_examples=when_to_use_examples,
|
|
135
|
+
metadata=metadata,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class CommandDefinition(BaseModel):
|
|
140
|
+
"""Command definition loaded from markdown file.
|
|
141
|
+
|
|
142
|
+
Commands are slash commands that users can invoke directly.
|
|
143
|
+
They define instructions for the agent to follow.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
name: str = Field(description="Command name (from filename, e.g., 'review')")
|
|
147
|
+
description: str = Field(default="", description="Command description")
|
|
148
|
+
argument_hint: str | None = Field(
|
|
149
|
+
default=None, description="Hint for command arguments"
|
|
150
|
+
)
|
|
151
|
+
allowed_tools: list[str] = Field(
|
|
152
|
+
default_factory=list, description="List of allowed tools for this command"
|
|
153
|
+
)
|
|
154
|
+
content: str = Field(default="", description="Command instructions/content")
|
|
155
|
+
source: str | None = Field(
|
|
156
|
+
default=None, description="Source file path for this command"
|
|
157
|
+
)
|
|
158
|
+
# Raw frontmatter for any additional fields
|
|
159
|
+
metadata: dict[str, Any] = Field(
|
|
160
|
+
default_factory=dict, description="Additional metadata from frontmatter"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def load(cls, command_path: Path) -> CommandDefinition:
|
|
165
|
+
"""Load a command definition from a markdown file.
|
|
166
|
+
|
|
167
|
+
Command markdown files have YAML frontmatter with:
|
|
168
|
+
- description: Command description
|
|
169
|
+
- argument-hint: Hint for command arguments (string or list)
|
|
170
|
+
- allowed-tools: List of allowed tools
|
|
171
|
+
|
|
172
|
+
The body of the markdown is the command instructions.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
command_path: Path to the command markdown file.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Loaded CommandDefinition instance.
|
|
179
|
+
"""
|
|
180
|
+
with open(command_path) as f:
|
|
181
|
+
post = frontmatter.load(f)
|
|
182
|
+
|
|
183
|
+
# Extract frontmatter fields with proper type handling
|
|
184
|
+
fm = post.metadata
|
|
185
|
+
name = command_path.stem # Command name from filename
|
|
186
|
+
description = str(fm.get("description", ""))
|
|
187
|
+
argument_hint_raw = fm.get("argument-hint") or fm.get("argumentHint")
|
|
188
|
+
allowed_tools_raw = fm.get("allowed-tools") or fm.get("allowedTools") or []
|
|
189
|
+
|
|
190
|
+
# Handle argument_hint as list (join with space) or string
|
|
191
|
+
argument_hint: str | None
|
|
192
|
+
if isinstance(argument_hint_raw, list):
|
|
193
|
+
argument_hint = " ".join(str(h) for h in argument_hint_raw)
|
|
194
|
+
elif argument_hint_raw is not None:
|
|
195
|
+
argument_hint = str(argument_hint_raw)
|
|
196
|
+
else:
|
|
197
|
+
argument_hint = None
|
|
198
|
+
|
|
199
|
+
# Ensure allowed_tools is a list of strings
|
|
200
|
+
allowed_tools: list[str]
|
|
201
|
+
if isinstance(allowed_tools_raw, str):
|
|
202
|
+
allowed_tools = [allowed_tools_raw]
|
|
203
|
+
elif isinstance(allowed_tools_raw, list):
|
|
204
|
+
allowed_tools = [str(t) for t in allowed_tools_raw]
|
|
205
|
+
else:
|
|
206
|
+
allowed_tools = []
|
|
207
|
+
|
|
208
|
+
# Remove known fields from metadata to get extras
|
|
209
|
+
known_fields = {
|
|
210
|
+
"description",
|
|
211
|
+
"argument-hint",
|
|
212
|
+
"argumentHint",
|
|
213
|
+
"allowed-tools",
|
|
214
|
+
"allowedTools",
|
|
215
|
+
}
|
|
216
|
+
metadata = {k: v for k, v in fm.items() if k not in known_fields}
|
|
217
|
+
|
|
218
|
+
return cls(
|
|
219
|
+
name=name,
|
|
220
|
+
description=description,
|
|
221
|
+
argument_hint=argument_hint,
|
|
222
|
+
allowed_tools=allowed_tools,
|
|
223
|
+
content=post.content.strip(),
|
|
224
|
+
source=str(command_path),
|
|
225
|
+
metadata=metadata,
|
|
226
|
+
)
|
openhands/sdk/tool/__init__.py
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
from openhands.sdk.tool.builtins import
|
|
1
|
+
from openhands.sdk.tool.builtins import (
|
|
2
|
+
BUILT_IN_TOOL_CLASSES,
|
|
3
|
+
BUILT_IN_TOOLS,
|
|
4
|
+
FinishTool,
|
|
5
|
+
ThinkTool,
|
|
6
|
+
)
|
|
2
7
|
from openhands.sdk.tool.registry import (
|
|
3
8
|
list_registered_tools,
|
|
4
9
|
register_tool,
|
|
@@ -28,6 +33,7 @@ __all__ = [
|
|
|
28
33
|
"FinishTool",
|
|
29
34
|
"ThinkTool",
|
|
30
35
|
"BUILT_IN_TOOLS",
|
|
36
|
+
"BUILT_IN_TOOL_CLASSES",
|
|
31
37
|
"register_tool",
|
|
32
38
|
"resolve_tool",
|
|
33
39
|
"list_registered_tools",
|
|
@@ -21,8 +21,12 @@ from openhands.sdk.tool.builtins.think import (
|
|
|
21
21
|
|
|
22
22
|
BUILT_IN_TOOLS = [FinishTool, ThinkTool]
|
|
23
23
|
|
|
24
|
+
# Mapping of built-in tool class names to their classes, generated dynamically
|
|
25
|
+
BUILT_IN_TOOL_CLASSES = {tool.__name__: tool for tool in BUILT_IN_TOOLS}
|
|
26
|
+
|
|
24
27
|
__all__ = [
|
|
25
28
|
"BUILT_IN_TOOLS",
|
|
29
|
+
"BUILT_IN_TOOL_CLASSES",
|
|
26
30
|
"FinishTool",
|
|
27
31
|
"FinishAction",
|
|
28
32
|
"FinishObservation",
|
openhands/sdk/tool/schema.py
CHANGED
|
@@ -121,9 +121,12 @@ class Schema(DiscriminatedUnionMixin):
|
|
|
121
121
|
# so it is fully compatible with MCP tool schema
|
|
122
122
|
result = _process_schema_node(full_schema, full_schema.get("$defs", {}))
|
|
123
123
|
|
|
124
|
-
# Remove
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
# Remove discriminator fields from properties (not for LLM)
|
|
125
|
+
# Need to exclude both regular fields and computed fields (like 'kind')
|
|
126
|
+
exclude_fields = set(DiscriminatedUnionMixin.model_fields.keys()) | set(
|
|
127
|
+
DiscriminatedUnionMixin.model_computed_fields.keys()
|
|
128
|
+
)
|
|
129
|
+
for f in exclude_fields:
|
|
127
130
|
if "properties" in result and f in result["properties"]:
|
|
128
131
|
result["properties"].pop(f)
|
|
129
132
|
# Also remove from required if present
|
openhands/sdk/tool/tool.py
CHANGED
|
@@ -41,6 +41,7 @@ if TYPE_CHECKING:
|
|
|
41
41
|
ActionT = TypeVar("ActionT", bound=Action)
|
|
42
42
|
ObservationT = TypeVar("ObservationT", bound=Observation)
|
|
43
43
|
_action_types_with_risk: dict[type, type] = {}
|
|
44
|
+
_action_types_with_summary: dict[type, type] = {}
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
def _camel_to_snake(name: str) -> str:
|
|
@@ -364,17 +365,18 @@ class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
|
|
|
364
365
|
action_type: type[Schema] | None = None,
|
|
365
366
|
) -> dict[str, Any]:
|
|
366
367
|
action_type = action_type or self.action_type
|
|
367
|
-
action_type_with_risk = create_action_type_with_risk(action_type)
|
|
368
368
|
|
|
369
|
+
# Apply security risk enhancement if enabled
|
|
369
370
|
add_security_risk_prediction = add_security_risk_prediction and (
|
|
370
371
|
self.annotations is None or (not self.annotations.readOnlyHint)
|
|
371
372
|
)
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
)
|
|
377
|
-
|
|
373
|
+
if add_security_risk_prediction:
|
|
374
|
+
action_type = create_action_type_with_risk(action_type)
|
|
375
|
+
|
|
376
|
+
# Always add summary field for transparency and explainability
|
|
377
|
+
action_type = _create_action_type_with_summary(action_type)
|
|
378
|
+
|
|
379
|
+
return action_type.to_mcp_schema()
|
|
378
380
|
|
|
379
381
|
def to_openai_tool(
|
|
380
382
|
self,
|
|
@@ -391,6 +393,10 @@ class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
|
|
|
391
393
|
action_type: Optionally override the action_type to use for the schema.
|
|
392
394
|
This is useful for MCPTool to use a dynamically created action type
|
|
393
395
|
based on the tool's input schema.
|
|
396
|
+
|
|
397
|
+
Note:
|
|
398
|
+
Summary field is always added to the schema for transparency and
|
|
399
|
+
explainability of agent actions.
|
|
394
400
|
"""
|
|
395
401
|
return ChatCompletionToolParam(
|
|
396
402
|
type="function",
|
|
@@ -398,7 +404,8 @@ class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
|
|
|
398
404
|
name=self.name,
|
|
399
405
|
description=self.description,
|
|
400
406
|
parameters=self._get_tool_schema(
|
|
401
|
-
add_security_risk_prediction,
|
|
407
|
+
add_security_risk_prediction,
|
|
408
|
+
action_type,
|
|
402
409
|
),
|
|
403
410
|
),
|
|
404
411
|
)
|
|
@@ -412,6 +419,14 @@ class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
|
|
|
412
419
|
|
|
413
420
|
For Responses API, function tools expect top-level keys:
|
|
414
421
|
{ "type": "function", "name": ..., "description": ..., "parameters": ... }
|
|
422
|
+
|
|
423
|
+
Args:
|
|
424
|
+
add_security_risk_prediction: Whether to add a `security_risk` field
|
|
425
|
+
action_type: Optional override for the action type
|
|
426
|
+
|
|
427
|
+
Note:
|
|
428
|
+
Summary field is always added to the schema for transparency and
|
|
429
|
+
explainability of agent actions.
|
|
415
430
|
"""
|
|
416
431
|
|
|
417
432
|
return {
|
|
@@ -419,7 +434,8 @@ class ToolDefinition[ActionT, ObservationT](DiscriminatedUnionMixin, ABC):
|
|
|
419
434
|
"name": self.name,
|
|
420
435
|
"description": self.description,
|
|
421
436
|
"parameters": self._get_tool_schema(
|
|
422
|
-
add_security_risk_prediction,
|
|
437
|
+
add_security_risk_prediction,
|
|
438
|
+
action_type,
|
|
423
439
|
),
|
|
424
440
|
"strict": False,
|
|
425
441
|
}
|
|
@@ -479,3 +495,38 @@ def create_action_type_with_risk(action_type: type[Schema]) -> type[Schema]:
|
|
|
479
495
|
)
|
|
480
496
|
_action_types_with_risk[action_type] = action_type_with_risk
|
|
481
497
|
return action_type_with_risk
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _create_action_type_with_summary(action_type: type[Schema]) -> type[Schema]:
|
|
501
|
+
"""Create a new action type with summary field for LLM to predict.
|
|
502
|
+
|
|
503
|
+
This dynamically adds a 'summary' field to the action schema, allowing
|
|
504
|
+
the LLM to provide a brief explanation of what each action does.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
action_type: The original action type to enhance
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
A new type that includes the summary field
|
|
511
|
+
"""
|
|
512
|
+
action_type_with_summary = _action_types_with_summary.get(action_type)
|
|
513
|
+
if action_type_with_summary:
|
|
514
|
+
return action_type_with_summary
|
|
515
|
+
|
|
516
|
+
action_type_with_summary = type(
|
|
517
|
+
f"{action_type.__name__}WithSummary",
|
|
518
|
+
(action_type,),
|
|
519
|
+
{
|
|
520
|
+
"summary": Field(
|
|
521
|
+
default=None,
|
|
522
|
+
description=(
|
|
523
|
+
"A concise summary (approximately 10 words) describing what "
|
|
524
|
+
"this specific action does. Focus on the key operation and target. "
|
|
525
|
+
"Example: 'List all Python files in current directory'"
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
"__annotations__": {"summary": str | None},
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
_action_types_with_summary[action_type] = action_type_with_summary
|
|
532
|
+
return action_type_with_summary
|