openhands-sdk 1.7.4__py3-none-any.whl → 1.8.1__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 +27 -0
- openhands/sdk/agent/base.py +88 -82
- 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/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 +275 -296
- openhands/sdk/context/skills/types.py +4 -0
- openhands/sdk/context/skills/utils.py +442 -0
- openhands/sdk/conversation/impl/local_conversation.py +42 -14
- openhands/sdk/conversation/state.py +52 -20
- 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 +58 -74
- openhands/sdk/llm/router/base.py +12 -0
- openhands/sdk/llm/utils/telemetry.py +2 -2
- 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/tool.py +60 -9
- openhands/sdk/workspace/remote/async_remote_workspace.py +16 -0
- openhands/sdk/workspace/remote/base.py +16 -0
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/METADATA +1 -1
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/RECORD +32 -28
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/WHEEL +0 -0
- {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/top_level.txt +0 -0
openhands/sdk/__init__.py
CHANGED
|
@@ -41,6 +41,7 @@ from openhands.sdk.mcp import (
|
|
|
41
41
|
MCPToolObservation,
|
|
42
42
|
create_mcp_tools,
|
|
43
43
|
)
|
|
44
|
+
from openhands.sdk.plugin import Plugin
|
|
44
45
|
from openhands.sdk.tool import (
|
|
45
46
|
Action,
|
|
46
47
|
Observation,
|
|
@@ -98,6 +99,7 @@ __all__ = [
|
|
|
98
99
|
"LLMSummarizingCondenser",
|
|
99
100
|
"FileStore",
|
|
100
101
|
"LocalFileStore",
|
|
102
|
+
"Plugin",
|
|
101
103
|
"register_tool",
|
|
102
104
|
"resolve_tool",
|
|
103
105
|
"list_registered_tools",
|
openhands/sdk/agent/agent.py
CHANGED
|
@@ -362,6 +362,30 @@ class Agent(AgentBase):
|
|
|
362
362
|
security_risk = risk.SecurityRisk(raw)
|
|
363
363
|
return security_risk
|
|
364
364
|
|
|
365
|
+
def _extract_summary(self, tool_name: str, arguments: dict) -> str:
|
|
366
|
+
"""Extract and validate the summary field from tool arguments.
|
|
367
|
+
|
|
368
|
+
Summary field is always requested but optional - if LLM doesn't provide
|
|
369
|
+
it or provides invalid data, we generate a default summary using the
|
|
370
|
+
tool name and arguments.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
tool_name: Name of the tool being called
|
|
374
|
+
arguments: Dictionary of tool arguments from LLM
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
The summary string - either from LLM or a default generated one
|
|
378
|
+
"""
|
|
379
|
+
summary = arguments.pop("summary", None)
|
|
380
|
+
|
|
381
|
+
# If valid summary provided by LLM, use it
|
|
382
|
+
if summary is not None and isinstance(summary, str) and summary.strip():
|
|
383
|
+
return summary
|
|
384
|
+
|
|
385
|
+
# Generate default summary: {tool_name}: {arguments}
|
|
386
|
+
args_str = json.dumps(arguments)
|
|
387
|
+
return f"{tool_name}: {args_str}"
|
|
388
|
+
|
|
365
389
|
def _get_action_event(
|
|
366
390
|
self,
|
|
367
391
|
tool_call: MessageToolCall,
|
|
@@ -423,6 +447,8 @@ class Agent(AgentBase):
|
|
|
423
447
|
"Unexpected 'security_risk' key found in tool arguments"
|
|
424
448
|
)
|
|
425
449
|
|
|
450
|
+
summary = self._extract_summary(tool.name, arguments)
|
|
451
|
+
|
|
426
452
|
action: Action = tool.action_from_arguments(arguments)
|
|
427
453
|
except (json.JSONDecodeError, ValidationError, ValueError) as e:
|
|
428
454
|
err = (
|
|
@@ -462,6 +488,7 @@ class Agent(AgentBase):
|
|
|
462
488
|
tool_call=tool_call,
|
|
463
489
|
llm_response_id=llm_response_id,
|
|
464
490
|
security_risk=security_risk,
|
|
491
|
+
summary=summary,
|
|
465
492
|
)
|
|
466
493
|
on_event(action_event)
|
|
467
494
|
return action_event
|
openhands/sdk/agent/base.py
CHANGED
|
@@ -6,18 +6,28 @@ from collections.abc import Generator, Iterable, Sequence
|
|
|
6
6
|
from concurrent.futures import ThreadPoolExecutor
|
|
7
7
|
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from pydantic import
|
|
9
|
+
from pydantic import (
|
|
10
|
+
BaseModel,
|
|
11
|
+
ConfigDict,
|
|
12
|
+
Field,
|
|
13
|
+
PrivateAttr,
|
|
14
|
+
)
|
|
10
15
|
|
|
11
16
|
from openhands.sdk.context.agent_context import AgentContext
|
|
12
|
-
from openhands.sdk.context.condenser import CondenserBase
|
|
17
|
+
from openhands.sdk.context.condenser import CondenserBase
|
|
13
18
|
from openhands.sdk.context.prompts.prompt import render_template
|
|
14
19
|
from openhands.sdk.llm import LLM
|
|
15
20
|
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
16
21
|
from openhands.sdk.logger import get_logger
|
|
17
22
|
from openhands.sdk.mcp import create_mcp_tools
|
|
18
|
-
from openhands.sdk.tool import
|
|
23
|
+
from openhands.sdk.tool import (
|
|
24
|
+
BUILT_IN_TOOL_CLASSES,
|
|
25
|
+
BUILT_IN_TOOLS,
|
|
26
|
+
Tool,
|
|
27
|
+
ToolDefinition,
|
|
28
|
+
resolve_tool,
|
|
29
|
+
)
|
|
19
30
|
from openhands.sdk.utils.models import DiscriminatedUnionMixin
|
|
20
|
-
from openhands.sdk.utils.pydantic_diff import pretty_pydantic_diff
|
|
21
31
|
|
|
22
32
|
|
|
23
33
|
if TYPE_CHECKING:
|
|
@@ -81,6 +91,17 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
81
91
|
" added.",
|
|
82
92
|
examples=["^(?!repomix)(.*)|^repomix.*pack_codebase.*$"],
|
|
83
93
|
)
|
|
94
|
+
include_default_tools: list[str] = Field(
|
|
95
|
+
default_factory=lambda: [tool.__name__ for tool in BUILT_IN_TOOLS],
|
|
96
|
+
description=(
|
|
97
|
+
"List of default tool class names to include. By default, the agent "
|
|
98
|
+
"includes 'FinishTool' and 'ThinkTool'. Set to an empty list to disable "
|
|
99
|
+
"all default tools, or provide a subset to include only specific ones. "
|
|
100
|
+
"Example: include_default_tools=['FinishTool'] to only include FinishTool, "
|
|
101
|
+
"or include_default_tools=[] to disable all default tools."
|
|
102
|
+
),
|
|
103
|
+
examples=[["FinishTool", "ThinkTool"], ["FinishTool"], []],
|
|
104
|
+
)
|
|
84
105
|
agent_context: AgentContext | None = Field(
|
|
85
106
|
default=None,
|
|
86
107
|
description="Optional AgentContext to initialize "
|
|
@@ -89,7 +110,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
89
110
|
{
|
|
90
111
|
"skills": [
|
|
91
112
|
{
|
|
92
|
-
"name": "
|
|
113
|
+
"name": "AGENTS.md",
|
|
93
114
|
"content": "When you see this message, you should reply like "
|
|
94
115
|
"you are a grumpy cat forced to use the internet.",
|
|
95
116
|
"type": "repo",
|
|
@@ -155,6 +176,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
155
176
|
|
|
156
177
|
# Runtime materialized tools; private and non-serializable
|
|
157
178
|
_tools: dict[str, ToolDefinition] = PrivateAttr(default_factory=dict)
|
|
179
|
+
_initialized: bool = PrivateAttr(default=False)
|
|
158
180
|
|
|
159
181
|
@property
|
|
160
182
|
def prompt_dir(self) -> str:
|
|
@@ -219,7 +241,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
219
241
|
def _initialize(self, state: "ConversationState"):
|
|
220
242
|
"""Create an AgentBase instance from an AgentSpec."""
|
|
221
243
|
|
|
222
|
-
if self.
|
|
244
|
+
if self._initialized:
|
|
223
245
|
logger.warning("Agent already initialized; skipping re-initialization.")
|
|
224
246
|
return
|
|
225
247
|
|
|
@@ -255,10 +277,17 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
255
277
|
f"{[tool.name for tool in tools]}",
|
|
256
278
|
)
|
|
257
279
|
|
|
258
|
-
#
|
|
259
|
-
#
|
|
260
|
-
for
|
|
261
|
-
|
|
280
|
+
# Include default tools from include_default_tools; not subject to regex
|
|
281
|
+
# filtering. Use explicit mapping to resolve tool class names.
|
|
282
|
+
for tool_name in self.include_default_tools:
|
|
283
|
+
tool_class = BUILT_IN_TOOL_CLASSES.get(tool_name)
|
|
284
|
+
if tool_class is None:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"Unknown built-in tool class: '{tool_name}'. "
|
|
287
|
+
f"Expected one of: {list(BUILT_IN_TOOL_CLASSES.keys())}"
|
|
288
|
+
)
|
|
289
|
+
tool_instances = tool_class.create(state)
|
|
290
|
+
tools.extend(tool_instances)
|
|
262
291
|
|
|
263
292
|
# Check tool types
|
|
264
293
|
for tool in tools:
|
|
@@ -276,6 +305,7 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
276
305
|
|
|
277
306
|
# Store tools in a dict for easy access
|
|
278
307
|
self._tools = {tool.name: tool for tool in tools}
|
|
308
|
+
self._initialized = True
|
|
279
309
|
|
|
280
310
|
@abstractmethod
|
|
281
311
|
def step(
|
|
@@ -300,64 +330,52 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
300
330
|
NOTE: state will be mutated in-place.
|
|
301
331
|
"""
|
|
302
332
|
|
|
303
|
-
def
|
|
333
|
+
def verify(
|
|
304
334
|
self,
|
|
305
335
|
persisted: "AgentBase",
|
|
306
336
|
events: "Sequence[Any] | None" = None,
|
|
307
337
|
) -> "AgentBase":
|
|
308
|
-
"""
|
|
309
|
-
|
|
310
|
-
|
|
338
|
+
"""Verify that we can resume this agent from persisted state.
|
|
339
|
+
|
|
340
|
+
This PR's goal is to *not* reconcile configuration between persisted and
|
|
341
|
+
runtime Agent instances. Instead, we verify compatibility requirements
|
|
342
|
+
and then continue with the runtime-provided Agent.
|
|
343
|
+
|
|
344
|
+
Compatibility requirements:
|
|
345
|
+
- Agent class/type must match.
|
|
346
|
+
- Tools:
|
|
347
|
+
- If events are provided, only tools that were actually used in history
|
|
348
|
+
must exist in runtime.
|
|
349
|
+
- If events are not provided, tool names must match exactly.
|
|
350
|
+
|
|
351
|
+
All other configuration (LLM, agent_context, condenser, system prompts,
|
|
352
|
+
etc.) can be freely changed between sessions.
|
|
311
353
|
|
|
312
354
|
Args:
|
|
313
|
-
persisted: The
|
|
314
|
-
events: Optional event sequence to scan for used tools if tool
|
|
315
|
-
|
|
355
|
+
persisted: The agent loaded from persisted state.
|
|
356
|
+
events: Optional event sequence to scan for used tools if tool names
|
|
357
|
+
don't match.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
This runtime agent (self) if verification passes.
|
|
361
|
+
|
|
362
|
+
Raises:
|
|
363
|
+
ValueError: If agent class or tools don't match.
|
|
316
364
|
"""
|
|
317
365
|
if persisted.__class__ is not self.__class__:
|
|
318
366
|
raise ValueError(
|
|
319
|
-
|
|
367
|
+
"Cannot load from persisted: persisted agent is of type "
|
|
320
368
|
f"{persisted.__class__.__name__}, but self is of type "
|
|
321
369
|
f"{self.__class__.__name__}."
|
|
322
370
|
)
|
|
323
371
|
|
|
324
|
-
# Get all LLMs from both self and persisted to reconcile them
|
|
325
|
-
new_llm = self.llm.resolve_diff_from_deserialized(persisted.llm)
|
|
326
|
-
updates: dict[str, Any] = {"llm": new_llm}
|
|
327
|
-
|
|
328
|
-
# Reconcile the condenser's LLM if it exists
|
|
329
|
-
if self.condenser is not None and persisted.condenser is not None:
|
|
330
|
-
# Check if both condensers are LLMSummarizingCondenser
|
|
331
|
-
# (which has an llm field)
|
|
332
|
-
|
|
333
|
-
if isinstance(self.condenser, LLMSummarizingCondenser) and isinstance(
|
|
334
|
-
persisted.condenser, LLMSummarizingCondenser
|
|
335
|
-
):
|
|
336
|
-
new_condenser_llm = self.condenser.llm.resolve_diff_from_deserialized(
|
|
337
|
-
persisted.condenser.llm
|
|
338
|
-
)
|
|
339
|
-
new_condenser = persisted.condenser.model_copy(
|
|
340
|
-
update={"llm": new_condenser_llm}
|
|
341
|
-
)
|
|
342
|
-
updates["condenser"] = new_condenser
|
|
343
|
-
|
|
344
|
-
# Reconcile agent_context - always use the current environment's agent_context
|
|
345
|
-
# This allows resuming conversations from different directories and handles
|
|
346
|
-
# cases where skills, working directory, or other context has changed
|
|
347
|
-
if self.agent_context is not None:
|
|
348
|
-
updates["agent_context"] = self.agent_context
|
|
349
|
-
|
|
350
|
-
# Get tool names for comparison
|
|
351
372
|
runtime_names = {tool.name for tool in self.tools}
|
|
352
373
|
persisted_names = {tool.name for tool in persisted.tools}
|
|
353
374
|
|
|
354
|
-
# If tool names match exactly, no need to check event history
|
|
355
375
|
if runtime_names == persisted_names:
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
# Tool names differ - scan events to find which tools were actually used
|
|
360
|
-
# This is O(n) but only happens when tools change
|
|
376
|
+
return self
|
|
377
|
+
|
|
378
|
+
if events is not None:
|
|
361
379
|
from openhands.sdk.event import ActionEvent
|
|
362
380
|
|
|
363
381
|
used_tools = {
|
|
@@ -366,43 +384,31 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
366
384
|
if isinstance(event, ActionEvent) and event.tool_name
|
|
367
385
|
}
|
|
368
386
|
|
|
369
|
-
# Only require tools that were actually used in history
|
|
387
|
+
# Only require tools that were actually used in history.
|
|
370
388
|
missing_used_tools = used_tools - runtime_names
|
|
371
389
|
if missing_used_tools:
|
|
372
390
|
raise ValueError(
|
|
373
|
-
|
|
391
|
+
"Cannot resume conversation: tools that were used in history "
|
|
374
392
|
f"are missing from runtime: {sorted(missing_used_tools)}. "
|
|
375
393
|
f"Available tools: {sorted(runtime_names)}"
|
|
376
394
|
)
|
|
377
|
-
# Update tools to match runtime (allows new tools to be added)
|
|
378
|
-
updates["tools"] = self.tools
|
|
379
|
-
else:
|
|
380
|
-
# No events provided - strict matching (legacy behavior)
|
|
381
|
-
missing_in_runtime = persisted_names - runtime_names
|
|
382
|
-
missing_in_persisted = runtime_names - persisted_names
|
|
383
|
-
error_msg = "Tools don't match between runtime and persisted agents."
|
|
384
|
-
if missing_in_runtime:
|
|
385
|
-
error_msg += f" Missing in runtime: {sorted(missing_in_runtime)}."
|
|
386
|
-
if missing_in_persisted:
|
|
387
|
-
error_msg += f" Missing in persisted: {sorted(missing_in_persisted)}."
|
|
388
|
-
raise ValueError(error_msg)
|
|
389
|
-
|
|
390
|
-
reconciled = persisted.model_copy(update=updates)
|
|
391
|
-
|
|
392
|
-
# Validate agent equality - exclude tools from comparison since we
|
|
393
|
-
# already validated tool requirements above
|
|
394
|
-
exclude_fields = {"tools"} if events is not None else set()
|
|
395
|
-
self_dump = self.model_dump(exclude_none=True, exclude=exclude_fields)
|
|
396
|
-
reconciled_dump = reconciled.model_dump(
|
|
397
|
-
exclude_none=True, exclude=exclude_fields
|
|
398
|
-
)
|
|
399
395
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
# No events provided: strict tool name matching.
|
|
399
|
+
missing_in_runtime = persisted_names - runtime_names
|
|
400
|
+
missing_in_persisted = runtime_names - persisted_names
|
|
401
|
+
|
|
402
|
+
details: list[str] = []
|
|
403
|
+
if missing_in_runtime:
|
|
404
|
+
details.append(f"Missing in runtime: {sorted(missing_in_runtime)}")
|
|
405
|
+
if missing_in_persisted:
|
|
406
|
+
details.append(f"Missing in persisted: {sorted(missing_in_persisted)}")
|
|
407
|
+
|
|
408
|
+
suffix = f" ({'; '.join(details)})" if details else ""
|
|
409
|
+
raise ValueError(
|
|
410
|
+
"Tools don't match between runtime and persisted agents." + suffix
|
|
411
|
+
)
|
|
406
412
|
|
|
407
413
|
def model_dump_succint(self, **kwargs):
|
|
408
414
|
"""Like model_dump, but excludes None fields by default."""
|
|
@@ -490,6 +496,6 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
490
496
|
Raises:
|
|
491
497
|
RuntimeError: If the agent has not been initialized.
|
|
492
498
|
"""
|
|
493
|
-
if not self.
|
|
499
|
+
if not self._initialized:
|
|
494
500
|
raise RuntimeError("Agent not initialized; call initialize() before use")
|
|
495
501
|
return self._tools
|
|
@@ -6,7 +6,7 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
6
6
|
</ROLE>
|
|
7
7
|
|
|
8
8
|
<MEMORY>
|
|
9
|
-
* Use
|
|
9
|
+
* Use `AGENTS.md` under the repository root as your persistent memory for repository-specific knowledge and context.
|
|
10
10
|
* Add important insights, patterns, and learnings to this file to improve future task performance.
|
|
11
11
|
* This repository skill is automatically loaded for every conversation and helps maintain context across sessions.
|
|
12
12
|
* For more information about skills, see: https://docs.openhands.dev/overview/skills
|
openhands/sdk/agent/utils.py
CHANGED
|
@@ -209,6 +209,9 @@ def make_llm_completion(
|
|
|
209
209
|
configured. This allows weaker models to omit risk field and bypass
|
|
210
210
|
validation requirements when analyzer is disabled. For detailed logic,
|
|
211
211
|
see `_extract_security_risk` method in agent.py.
|
|
212
|
+
|
|
213
|
+
Summary field is always added to tool schemas for transparency and
|
|
214
|
+
explainability of agent actions.
|
|
212
215
|
"""
|
|
213
216
|
if llm.uses_responses_api():
|
|
214
217
|
return llm.responses(
|
|
@@ -11,6 +11,7 @@ from openhands.sdk.context.skills import (
|
|
|
11
11
|
SkillKnowledge,
|
|
12
12
|
load_public_skills,
|
|
13
13
|
load_user_skills,
|
|
14
|
+
to_prompt,
|
|
14
15
|
)
|
|
15
16
|
from openhands.sdk.llm import Message, TextContent
|
|
16
17
|
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
@@ -167,8 +168,32 @@ class AgentContext(BaseModel):
|
|
|
167
168
|
- Runtime information (e.g., available hosts, current date)
|
|
168
169
|
- Conversation instructions (e.g., user preferences, task details)
|
|
169
170
|
- Repository-specific instructions (collected from repo skills)
|
|
171
|
+
- Available skills list (for AgentSkills-format and triggered skills)
|
|
172
|
+
|
|
173
|
+
Skill categorization:
|
|
174
|
+
- AgentSkills-format (SKILL.md): Always in <available_skills> (progressive
|
|
175
|
+
disclosure). If has triggers, content is ALSO auto-injected on trigger
|
|
176
|
+
in user prompts.
|
|
177
|
+
- Legacy with trigger=None: Full content in <REPO_CONTEXT> (always active)
|
|
178
|
+
- Legacy with triggers: Listed in <available_skills>, injected on trigger
|
|
170
179
|
"""
|
|
171
|
-
|
|
180
|
+
# Categorize skills based on format and trigger:
|
|
181
|
+
# - AgentSkills-format: always in available_skills (progressive disclosure)
|
|
182
|
+
# - Legacy: trigger=None -> REPO_CONTEXT, else -> available_skills
|
|
183
|
+
repo_skills: list[Skill] = []
|
|
184
|
+
available_skills: list[Skill] = []
|
|
185
|
+
|
|
186
|
+
for s in self.skills:
|
|
187
|
+
if s.is_agentskills_format:
|
|
188
|
+
# AgentSkills: always list (triggers also auto-inject via
|
|
189
|
+
# get_user_message_suffix)
|
|
190
|
+
available_skills.append(s)
|
|
191
|
+
elif s.trigger is None:
|
|
192
|
+
# Legacy OpenHands: no trigger = full content in REPO_CONTEXT
|
|
193
|
+
repo_skills.append(s)
|
|
194
|
+
else:
|
|
195
|
+
# Legacy OpenHands: has trigger = list in available_skills
|
|
196
|
+
available_skills.append(s)
|
|
172
197
|
|
|
173
198
|
# Gate vendor-specific repo skills based on model family.
|
|
174
199
|
if llm_model or llm_model_canonical:
|
|
@@ -189,16 +214,32 @@ class AgentContext(BaseModel):
|
|
|
189
214
|
filtered.append(s)
|
|
190
215
|
repo_skills = filtered
|
|
191
216
|
|
|
192
|
-
logger.debug(f"
|
|
217
|
+
logger.debug(f"Loaded {len(repo_skills)} repository skills: {repo_skills}")
|
|
218
|
+
|
|
219
|
+
# Generate available skills prompt
|
|
220
|
+
available_skills_prompt = ""
|
|
221
|
+
if available_skills:
|
|
222
|
+
available_skills_prompt = to_prompt(available_skills)
|
|
223
|
+
logger.debug(
|
|
224
|
+
f"Generated available skills prompt for {len(available_skills)} skills"
|
|
225
|
+
)
|
|
226
|
+
|
|
193
227
|
# Build the workspace context information
|
|
194
228
|
secret_infos = self.get_secret_infos()
|
|
195
|
-
|
|
229
|
+
has_content = (
|
|
230
|
+
repo_skills
|
|
231
|
+
or self.system_message_suffix
|
|
232
|
+
or secret_infos
|
|
233
|
+
or available_skills_prompt
|
|
234
|
+
)
|
|
235
|
+
if has_content:
|
|
196
236
|
formatted_text = render_template(
|
|
197
237
|
prompt_dir=str(PROMPT_DIR),
|
|
198
238
|
template_name="system_message_suffix.j2",
|
|
199
239
|
repo_skills=repo_skills,
|
|
200
240
|
system_message_suffix=self.system_message_suffix or "",
|
|
201
241
|
secret_infos=secret_infos,
|
|
242
|
+
available_skills_prompt=available_skills_prompt,
|
|
202
243
|
).strip()
|
|
203
244
|
return formatted_text
|
|
204
245
|
elif self.system_message_suffix and self.system_message_suffix.strip():
|
|
@@ -245,6 +286,7 @@ class AgentContext(BaseModel):
|
|
|
245
286
|
name=skill.name,
|
|
246
287
|
trigger=trigger,
|
|
247
288
|
content=skill.content,
|
|
289
|
+
location=skill.source,
|
|
248
290
|
)
|
|
249
291
|
)
|
|
250
292
|
if recalled_knowledge:
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
<EXTRA_INFO>
|
|
3
3
|
The following information has been included based on a keyword match for "{{ agent_info.trigger }}".
|
|
4
4
|
It may or may not be relevant to the user's request.
|
|
5
|
+
{% if agent_info.location %}
|
|
6
|
+
Skill location: {{ agent_info.location }}
|
|
7
|
+
(Use this path to resolve relative file references in the skill content below)
|
|
8
|
+
{% endif %}
|
|
5
9
|
|
|
6
10
|
{{ agent_info.content }}
|
|
7
11
|
</EXTRA_INFO>
|
|
@@ -10,6 +10,15 @@ Please follow them while working.
|
|
|
10
10
|
{% endfor %}
|
|
11
11
|
</REPO_CONTEXT>
|
|
12
12
|
{% endif %}
|
|
13
|
+
{% if available_skills_prompt %}
|
|
14
|
+
<SKILLS>
|
|
15
|
+
The following skills are available and may be triggered by keywords or task types in your messages.
|
|
16
|
+
When a skill is triggered, you will receive additional context and instructions.
|
|
17
|
+
You can also directly look up a skill's full content by reading its location path, and use the skill's guidance proactively when relevant to your task.
|
|
18
|
+
|
|
19
|
+
{{ available_skills_prompt }}
|
|
20
|
+
</SKILLS>
|
|
21
|
+
{% endif %}
|
|
13
22
|
{% if system_message_suffix %}
|
|
14
23
|
|
|
15
24
|
{{ system_message_suffix }}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
from openhands.sdk.context.skills.exceptions import SkillValidationError
|
|
2
2
|
from openhands.sdk.context.skills.skill import (
|
|
3
3
|
Skill,
|
|
4
|
+
SkillResources,
|
|
4
5
|
load_project_skills,
|
|
5
6
|
load_public_skills,
|
|
6
7
|
load_skills_from_dir,
|
|
7
8
|
load_user_skills,
|
|
9
|
+
to_prompt,
|
|
8
10
|
)
|
|
9
11
|
from openhands.sdk.context.skills.trigger import (
|
|
10
12
|
BaseTrigger,
|
|
@@ -12,10 +14,16 @@ from openhands.sdk.context.skills.trigger import (
|
|
|
12
14
|
TaskTrigger,
|
|
13
15
|
)
|
|
14
16
|
from openhands.sdk.context.skills.types import SkillKnowledge
|
|
17
|
+
from openhands.sdk.context.skills.utils import (
|
|
18
|
+
RESOURCE_DIRECTORIES,
|
|
19
|
+
discover_skill_resources,
|
|
20
|
+
validate_skill_name,
|
|
21
|
+
)
|
|
15
22
|
|
|
16
23
|
|
|
17
24
|
__all__ = [
|
|
18
25
|
"Skill",
|
|
26
|
+
"SkillResources",
|
|
19
27
|
"BaseTrigger",
|
|
20
28
|
"KeywordTrigger",
|
|
21
29
|
"TaskTrigger",
|
|
@@ -25,4 +33,8 @@ __all__ = [
|
|
|
25
33
|
"load_project_skills",
|
|
26
34
|
"load_public_skills",
|
|
27
35
|
"SkillValidationError",
|
|
36
|
+
"discover_skill_resources",
|
|
37
|
+
"RESOURCE_DIRECTORIES",
|
|
38
|
+
"to_prompt",
|
|
39
|
+
"validate_skill_name",
|
|
28
40
|
]
|