openhands-sdk 1.5.0__py3-none-any.whl → 1.7.2__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 +9 -1
- openhands/sdk/agent/agent.py +35 -12
- openhands/sdk/agent/base.py +53 -7
- openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
- openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +2 -0
- openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
- openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
- openhands/sdk/agent/prompts/system_prompt.j2 +29 -1
- openhands/sdk/agent/utils.py +18 -4
- openhands/sdk/context/__init__.py +2 -0
- openhands/sdk/context/agent_context.py +42 -10
- openhands/sdk/context/condenser/base.py +11 -6
- openhands/sdk/context/condenser/llm_summarizing_condenser.py +169 -20
- openhands/sdk/context/condenser/no_op_condenser.py +2 -1
- openhands/sdk/context/condenser/pipeline_condenser.py +10 -9
- openhands/sdk/context/condenser/utils.py +149 -0
- openhands/sdk/context/prompts/prompt.py +40 -2
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
- openhands/sdk/context/skills/__init__.py +2 -0
- openhands/sdk/context/skills/skill.py +152 -1
- openhands/sdk/context/view.py +287 -27
- openhands/sdk/conversation/base.py +17 -0
- openhands/sdk/conversation/conversation.py +19 -0
- openhands/sdk/conversation/exceptions.py +29 -4
- openhands/sdk/conversation/impl/local_conversation.py +126 -9
- openhands/sdk/conversation/impl/remote_conversation.py +152 -3
- openhands/sdk/conversation/state.py +42 -1
- openhands/sdk/conversation/stuck_detector.py +81 -45
- openhands/sdk/conversation/types.py +30 -0
- openhands/sdk/event/llm_convertible/system.py +16 -20
- openhands/sdk/hooks/__init__.py +30 -0
- openhands/sdk/hooks/config.py +180 -0
- openhands/sdk/hooks/conversation_hooks.py +227 -0
- openhands/sdk/hooks/executor.py +155 -0
- openhands/sdk/hooks/manager.py +170 -0
- openhands/sdk/hooks/types.py +40 -0
- openhands/sdk/io/cache.py +85 -0
- openhands/sdk/io/local.py +39 -2
- openhands/sdk/llm/llm.py +3 -2
- openhands/sdk/llm/message.py +4 -3
- openhands/sdk/llm/mixins/fn_call_converter.py +61 -16
- openhands/sdk/llm/mixins/non_native_fc.py +5 -1
- openhands/sdk/llm/utils/model_features.py +64 -24
- openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
- openhands/sdk/llm/utils/verified_models.py +6 -4
- openhands/sdk/logger/logger.py +1 -1
- openhands/sdk/tool/schema.py +10 -0
- openhands/sdk/tool/tool.py +2 -2
- openhands/sdk/utils/async_executor.py +76 -67
- openhands/sdk/utils/models.py +1 -1
- openhands/sdk/utils/paging.py +63 -0
- {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/METADATA +3 -3
- {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/RECORD +56 -41
- {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/WHEEL +0 -0
- {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/top_level.txt +0 -0
openhands/sdk/__init__.py
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
from openhands.sdk.agent import Agent, AgentBase
|
|
4
|
-
from openhands.sdk.context import
|
|
4
|
+
from openhands.sdk.context import (
|
|
5
|
+
AgentContext,
|
|
6
|
+
load_project_skills,
|
|
7
|
+
load_skills_from_dir,
|
|
8
|
+
load_user_skills,
|
|
9
|
+
)
|
|
5
10
|
from openhands.sdk.context.condenser import (
|
|
6
11
|
LLMSummarizingCondenser,
|
|
7
12
|
)
|
|
@@ -99,5 +104,8 @@ __all__ = [
|
|
|
99
104
|
"Workspace",
|
|
100
105
|
"LocalWorkspace",
|
|
101
106
|
"RemoteWorkspace",
|
|
107
|
+
"load_project_skills",
|
|
108
|
+
"load_skills_from_dir",
|
|
109
|
+
"load_user_skills",
|
|
102
110
|
"__version__",
|
|
103
111
|
]
|
openhands/sdk/agent/agent.py
CHANGED
|
@@ -25,6 +25,7 @@ from openhands.sdk.event import (
|
|
|
25
25
|
ObservationEvent,
|
|
26
26
|
SystemPromptEvent,
|
|
27
27
|
TokenEvent,
|
|
28
|
+
UserRejectObservation,
|
|
28
29
|
)
|
|
29
30
|
from openhands.sdk.event.condenser import Condensation, CondensationRequest
|
|
30
31
|
from openhands.sdk.llm import (
|
|
@@ -109,17 +110,10 @@ class Agent(AgentBase):
|
|
|
109
110
|
event = SystemPromptEvent(
|
|
110
111
|
source="agent",
|
|
111
112
|
system_prompt=TextContent(text=self.system_message),
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
# configured. This allows weaker models to omit risk field
|
|
117
|
-
# and bypass validation requirements when analyzer is disabled.
|
|
118
|
-
# For detailed logic, see `_extract_security_risk` method.
|
|
119
|
-
tools=[
|
|
120
|
-
t.to_openai_tool(add_security_risk_prediction=True)
|
|
121
|
-
for t in self.tools_map.values()
|
|
122
|
-
],
|
|
113
|
+
# Tools are stored as ToolDefinition objects and converted to
|
|
114
|
+
# OpenAI format with security_risk parameter during LLM completion.
|
|
115
|
+
# See make_llm_completion() in agent/utils.py for details.
|
|
116
|
+
tools=list(self.tools_map.values()),
|
|
123
117
|
)
|
|
124
118
|
on_event(event)
|
|
125
119
|
|
|
@@ -151,9 +145,20 @@ class Agent(AgentBase):
|
|
|
151
145
|
self._execute_actions(conversation, pending_actions, on_event)
|
|
152
146
|
return
|
|
153
147
|
|
|
148
|
+
# Check if the last user message was blocked by a UserPromptSubmit hook
|
|
149
|
+
# If so, skip processing and mark conversation as finished
|
|
150
|
+
for event in reversed(list(state.events)):
|
|
151
|
+
if isinstance(event, MessageEvent) and event.source == "user":
|
|
152
|
+
reason = state.pop_blocked_message(event.id)
|
|
153
|
+
if reason is not None:
|
|
154
|
+
logger.info(f"User message blocked by hook: {reason}")
|
|
155
|
+
state.execution_status = ConversationExecutionStatus.FINISHED
|
|
156
|
+
return
|
|
157
|
+
break # Only check the most recent user message
|
|
158
|
+
|
|
154
159
|
# Prepare LLM messages using the utility function
|
|
155
160
|
_messages_or_condensation = prepare_llm_messages(
|
|
156
|
-
state.events, condenser=self.condenser
|
|
161
|
+
state.events, condenser=self.condenser, llm=self.llm
|
|
157
162
|
)
|
|
158
163
|
|
|
159
164
|
# Process condensation event before agent sampels another action
|
|
@@ -469,8 +474,26 @@ class Agent(AgentBase):
|
|
|
469
474
|
|
|
470
475
|
It will call the tool's executor and update the state & call callback fn
|
|
471
476
|
with the observation.
|
|
477
|
+
|
|
478
|
+
If the action was blocked by a PreToolUse hook (recorded in
|
|
479
|
+
state.blocked_actions), a UserRejectObservation is emitted instead
|
|
480
|
+
of executing the action.
|
|
472
481
|
"""
|
|
473
482
|
state = conversation.state
|
|
483
|
+
|
|
484
|
+
# Check if this action was blocked by a PreToolUse hook
|
|
485
|
+
reason = state.pop_blocked_action(action_event.id)
|
|
486
|
+
if reason is not None:
|
|
487
|
+
logger.info(f"Action '{action_event.tool_name}' blocked by hook: {reason}")
|
|
488
|
+
rejection = UserRejectObservation(
|
|
489
|
+
action_id=action_event.id,
|
|
490
|
+
tool_name=action_event.tool_name,
|
|
491
|
+
tool_call_id=action_event.tool_call_id,
|
|
492
|
+
rejection_reason=reason,
|
|
493
|
+
)
|
|
494
|
+
on_event(rejection)
|
|
495
|
+
return rejection
|
|
496
|
+
|
|
474
497
|
tool = self.tools_map.get(action_event.tool_name, None)
|
|
475
498
|
if tool is None:
|
|
476
499
|
raise RuntimeError(
|
openhands/sdk/agent/base.py
CHANGED
|
@@ -3,6 +3,7 @@ import re
|
|
|
3
3
|
import sys
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
5
|
from collections.abc import Generator, Iterable
|
|
6
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
6
7
|
from typing import TYPE_CHECKING, Any
|
|
7
8
|
|
|
8
9
|
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
|
|
@@ -11,6 +12,7 @@ from openhands.sdk.context.agent_context import AgentContext
|
|
|
11
12
|
from openhands.sdk.context.condenser import CondenserBase, LLMSummarizingCondenser
|
|
12
13
|
from openhands.sdk.context.prompts.prompt import render_template
|
|
13
14
|
from openhands.sdk.llm import LLM
|
|
15
|
+
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
14
16
|
from openhands.sdk.logger import get_logger
|
|
15
17
|
from openhands.sdk.mcp import create_mcp_tools
|
|
16
18
|
from openhands.sdk.tool import BUILT_IN_TOOLS, Tool, ToolDefinition, resolve_tool
|
|
@@ -119,6 +121,15 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
119
121
|
"- An absolute path (e.g., '/path/to/custom_prompt.j2')"
|
|
120
122
|
),
|
|
121
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
|
+
)
|
|
122
133
|
system_prompt_kwargs: dict[str, object] = Field(
|
|
123
134
|
default_factory=dict,
|
|
124
135
|
description="Optional kwargs to pass to the system prompt Jinja2 template.",
|
|
@@ -163,13 +174,30 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
163
174
|
def system_message(self) -> str:
|
|
164
175
|
"""Compute system message on-demand to maintain statelessness."""
|
|
165
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
|
|
166
191
|
system_message = render_template(
|
|
167
192
|
prompt_dir=self.prompt_dir,
|
|
168
193
|
template_name=self.system_prompt_filename,
|
|
169
194
|
**template_kwargs,
|
|
170
195
|
)
|
|
171
196
|
if self.agent_context:
|
|
172
|
-
_system_message_suffix = self.agent_context.get_system_message_suffix(
|
|
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
|
+
)
|
|
173
201
|
if _system_message_suffix:
|
|
174
202
|
system_message += "\n\n" + _system_message_suffix
|
|
175
203
|
return system_message
|
|
@@ -196,13 +224,25 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
196
224
|
return
|
|
197
225
|
|
|
198
226
|
tools: list[ToolDefinition] = []
|
|
199
|
-
for tool_spec in self.tools:
|
|
200
|
-
tools.extend(resolve_tool(tool_spec, state))
|
|
201
227
|
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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)
|
|
206
246
|
|
|
207
247
|
logger.info(
|
|
208
248
|
f"Loaded {len(tools)} tools from spec: {[tool.name for tool in tools]}"
|
|
@@ -292,6 +332,12 @@ class AgentBase(DiscriminatedUnionMixin, ABC):
|
|
|
292
332
|
)
|
|
293
333
|
updates["condenser"] = new_condenser
|
|
294
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
|
+
|
|
295
341
|
# Create maps by tool name for easy lookup
|
|
296
342
|
runtime_tools_map = {tool.name: tool for tool in self.tools}
|
|
297
343
|
persisted_tools_map = {tool.name: tool for tool in persisted.tools}
|
|
@@ -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,15 @@
|
|
|
1
|
+
When the user directly asks about any of the following:
|
|
2
|
+
- OpenHands capabilities (e.g., "can OpenHands do...", "does OpenHands have...")
|
|
3
|
+
- what you're able to do in second person (e.g., "are you able...", "can you...")
|
|
4
|
+
- how to use a specific OpenHands feature or product
|
|
5
|
+
- how to use the OpenHands SDK, CLI, GUI, or other OpenHands products
|
|
6
|
+
|
|
7
|
+
Get accurate information from the official OpenHands documentation at <https://docs.openhands.dev/>. The documentation includes:
|
|
8
|
+
|
|
9
|
+
**OpenHands SDK** (`/sdk/*`): Python library for building AI agents; Getting Started, Architecture, Guides (agent, llm, conversation, tools), API Reference
|
|
10
|
+
**OpenHands CLI** (`/openhands/usage/run-openhands/cli-mode`): Command-line interface
|
|
11
|
+
**OpenHands GUI** (`/openhands/usage/run-openhands/local-setup`): Local GUI and REST API
|
|
12
|
+
**OpenHands Cloud** (`/openhands/usage/run-openhands/cloud`): Hosted solution with integrations
|
|
13
|
+
**OpenHands Enterprise**: Self-hosted deployment with extended support
|
|
14
|
+
|
|
15
|
+
Always provide links to the relevant documentation pages for users who want to learn more.
|
|
@@ -5,6 +5,13 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
5
5
|
* If the user asks a question, like "why is X happening", don't try to fix the problem. Just give an answer to the question.
|
|
6
6
|
</ROLE>
|
|
7
7
|
|
|
8
|
+
<MEMORY>
|
|
9
|
+
* Use `.openhands/skills/repo.md` under the repository root as your persistent memory for repository-specific knowledge and context.
|
|
10
|
+
* Add important insights, patterns, and learnings to this file to improve future task performance.
|
|
11
|
+
* This repository skill is automatically loaded for every conversation and helps maintain context across sessions.
|
|
12
|
+
* For more information about skills, see: https://docs.openhands.dev/overview/skills
|
|
13
|
+
</MEMORY>
|
|
14
|
+
|
|
8
15
|
<EFFICIENCY>
|
|
9
16
|
* Each action you take is somewhat expensive. Wherever possible, combine multiple actions into a single action, e.g. combine multiple bash commands into one, using sed and grep to edit/view multiple files at once.
|
|
10
17
|
* When exploring the codebase, use efficient tools like find, grep, and git commands with appropriate filters to minimize unnecessary operations.
|
|
@@ -62,8 +69,12 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
62
69
|
5. VERIFICATION: If the environment is set up to run tests, test your implementation thoroughly, including edge cases. If the environment is not set up to run tests, consult with the user first before investing time to run tests.
|
|
63
70
|
</PROBLEM_SOLVING_WORKFLOW>
|
|
64
71
|
|
|
72
|
+
<SELF_DOCUMENTATION>
|
|
73
|
+
{% include 'self_documentation.j2' %}
|
|
74
|
+
</SELF_DOCUMENTATION>
|
|
75
|
+
|
|
65
76
|
<SECURITY>
|
|
66
|
-
{% include
|
|
77
|
+
{% include security_policy_filename %}
|
|
67
78
|
</SECURITY>
|
|
68
79
|
|
|
69
80
|
{% if llm_security_analyzer %}
|
|
@@ -102,3 +113,20 @@ You are OpenHands agent, a helpful AI assistant that can interact with a compute
|
|
|
102
113
|
- Prefer using `ps aux` to find the exact process ID (PID) first, then kill that specific PID
|
|
103
114
|
- When possible, use more targeted approaches like finding the PID from a pidfile or using application-specific shutdown commands
|
|
104
115
|
</PROCESS_MANAGEMENT>
|
|
116
|
+
|
|
117
|
+
{%- set _imp -%}
|
|
118
|
+
{%- if model_family -%}
|
|
119
|
+
{%- include "model_specific/" ~ model_family ~ ".j2" ignore missing -%}
|
|
120
|
+
{%- if model_variant -%}
|
|
121
|
+
{%- include "model_specific/" ~ model_family ~ "/" ~ model_variant ~ ".j2" ignore missing -%}
|
|
122
|
+
{%- endif -%}
|
|
123
|
+
{%- endif -%}
|
|
124
|
+
{%- endset -%}
|
|
125
|
+
|
|
126
|
+
{%- set _imp_trimmed = _imp | trim -%}
|
|
127
|
+
{%- if _imp_trimmed %}
|
|
128
|
+
|
|
129
|
+
<IMPORTANT>
|
|
130
|
+
{{ _imp_trimmed }}
|
|
131
|
+
</IMPORTANT>
|
|
132
|
+
{%- endif %}
|
openhands/sdk/agent/utils.py
CHANGED
|
@@ -117,6 +117,7 @@ def prepare_llm_messages(
|
|
|
117
117
|
events: Sequence[Event],
|
|
118
118
|
condenser: None = None,
|
|
119
119
|
additional_messages: list[Message] | None = None,
|
|
120
|
+
llm: LLM | None = None,
|
|
120
121
|
) -> list[Message]: ...
|
|
121
122
|
|
|
122
123
|
|
|
@@ -125,6 +126,7 @@ def prepare_llm_messages(
|
|
|
125
126
|
events: Sequence[Event],
|
|
126
127
|
condenser: CondenserBase,
|
|
127
128
|
additional_messages: list[Message] | None = None,
|
|
129
|
+
llm: LLM | None = None,
|
|
128
130
|
) -> list[Message] | Condensation: ...
|
|
129
131
|
|
|
130
132
|
|
|
@@ -132,6 +134,7 @@ def prepare_llm_messages(
|
|
|
132
134
|
events: Sequence[Event],
|
|
133
135
|
condenser: CondenserBase | None = None,
|
|
134
136
|
additional_messages: list[Message] | None = None,
|
|
137
|
+
llm: LLM | None = None,
|
|
135
138
|
) -> list[Message] | Condensation:
|
|
136
139
|
"""Prepare LLM messages from conversation context.
|
|
137
140
|
|
|
@@ -140,13 +143,15 @@ def prepare_llm_messages(
|
|
|
140
143
|
It handles condensation internally and calls the callback when needed.
|
|
141
144
|
|
|
142
145
|
Args:
|
|
143
|
-
|
|
146
|
+
events: Sequence of events to prepare messages from
|
|
144
147
|
condenser: Optional condenser for handling context window limits
|
|
145
148
|
additional_messages: Optional additional messages to append
|
|
146
|
-
|
|
149
|
+
llm: Optional LLM instance from the agent, passed to condenser for
|
|
150
|
+
token counting or other LLM features
|
|
147
151
|
|
|
148
152
|
Returns:
|
|
149
|
-
List of messages ready for LLM completion
|
|
153
|
+
List of messages ready for LLM completion, or a Condensation event
|
|
154
|
+
if condensation is needed
|
|
150
155
|
|
|
151
156
|
Raises:
|
|
152
157
|
RuntimeError: If condensation is needed but no callback is provided
|
|
@@ -160,7 +165,7 @@ def prepare_llm_messages(
|
|
|
160
165
|
# produce a list of events, exactly as expected, or a
|
|
161
166
|
# new condensation that needs to be processed
|
|
162
167
|
if condenser is not None:
|
|
163
|
-
condensation_result = condenser.condense(view)
|
|
168
|
+
condensation_result = condenser.condense(view, agent_llm=llm)
|
|
164
169
|
|
|
165
170
|
match condensation_result:
|
|
166
171
|
case View():
|
|
@@ -195,6 +200,15 @@ def make_llm_completion(
|
|
|
195
200
|
|
|
196
201
|
Returns:
|
|
197
202
|
LLMResponse from the LLM completion call
|
|
203
|
+
|
|
204
|
+
Note:
|
|
205
|
+
Always exposes a 'security_risk' parameter in tool schemas via
|
|
206
|
+
add_security_risk_prediction=True. This ensures the schema remains
|
|
207
|
+
consistent, even if the security analyzer is disabled. Validation of
|
|
208
|
+
this field happens dynamically at runtime depending on the analyzer
|
|
209
|
+
configured. This allows weaker models to omit risk field and bypass
|
|
210
|
+
validation requirements when analyzer is disabled. For detailed logic,
|
|
211
|
+
see `_extract_security_risk` method in agent.py.
|
|
198
212
|
"""
|
|
199
213
|
if llm.uses_responses_api():
|
|
200
214
|
return llm.responses(
|
|
@@ -7,6 +7,7 @@ from openhands.sdk.context.skills import (
|
|
|
7
7
|
SkillKnowledge,
|
|
8
8
|
SkillValidationError,
|
|
9
9
|
TaskTrigger,
|
|
10
|
+
load_project_skills,
|
|
10
11
|
load_skills_from_dir,
|
|
11
12
|
load_user_skills,
|
|
12
13
|
)
|
|
@@ -21,6 +22,7 @@ __all__ = [
|
|
|
21
22
|
"SkillKnowledge",
|
|
22
23
|
"load_skills_from_dir",
|
|
23
24
|
"load_user_skills",
|
|
25
|
+
"load_project_skills",
|
|
24
26
|
"render_template",
|
|
25
27
|
"SkillValidationError",
|
|
26
28
|
]
|
|
@@ -13,8 +13,9 @@ from openhands.sdk.context.skills import (
|
|
|
13
13
|
load_user_skills,
|
|
14
14
|
)
|
|
15
15
|
from openhands.sdk.llm import Message, TextContent
|
|
16
|
+
from openhands.sdk.llm.utils.model_prompt_spec import get_model_prompt_spec
|
|
16
17
|
from openhands.sdk.logger import get_logger
|
|
17
|
-
from openhands.sdk.secret import SecretValue
|
|
18
|
+
from openhands.sdk.secret import SecretSource, SecretValue
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
logger = get_logger(__name__)
|
|
@@ -136,17 +137,29 @@ class AgentContext(BaseModel):
|
|
|
136
137
|
logger.warning(f"Failed to load public skills: {str(e)}")
|
|
137
138
|
return self
|
|
138
139
|
|
|
139
|
-
def
|
|
140
|
-
"""Get
|
|
140
|
+
def get_secret_infos(self) -> list[dict[str, str]]:
|
|
141
|
+
"""Get secret information (name and description) from the secrets field.
|
|
141
142
|
|
|
142
143
|
Returns:
|
|
143
|
-
List of
|
|
144
|
+
List of dictionaries with 'name' and 'description' keys.
|
|
145
|
+
Returns an empty list if no secrets are configured.
|
|
146
|
+
Description will be None if not available.
|
|
144
147
|
"""
|
|
145
148
|
if not self.secrets:
|
|
146
149
|
return []
|
|
147
|
-
|
|
150
|
+
secret_infos = []
|
|
151
|
+
for name, secret_value in self.secrets.items():
|
|
152
|
+
description = None
|
|
153
|
+
if isinstance(secret_value, SecretSource):
|
|
154
|
+
description = secret_value.description
|
|
155
|
+
secret_infos.append({"name": name, "description": description})
|
|
156
|
+
return secret_infos
|
|
148
157
|
|
|
149
|
-
def get_system_message_suffix(
|
|
158
|
+
def get_system_message_suffix(
|
|
159
|
+
self,
|
|
160
|
+
llm_model: str | None = None,
|
|
161
|
+
llm_model_canonical: str | None = None,
|
|
162
|
+
) -> str | None:
|
|
150
163
|
"""Get the system message with repo skill content and custom suffix.
|
|
151
164
|
|
|
152
165
|
Custom suffix can typically includes:
|
|
@@ -156,17 +169,36 @@ class AgentContext(BaseModel):
|
|
|
156
169
|
- Repository-specific instructions (collected from repo skills)
|
|
157
170
|
"""
|
|
158
171
|
repo_skills = [s for s in self.skills if s.trigger is None]
|
|
172
|
+
|
|
173
|
+
# Gate vendor-specific repo skills based on model family.
|
|
174
|
+
if llm_model or llm_model_canonical:
|
|
175
|
+
spec = get_model_prompt_spec(llm_model or "", llm_model_canonical)
|
|
176
|
+
family = (spec.family or "").lower()
|
|
177
|
+
if family:
|
|
178
|
+
filtered: list[Skill] = []
|
|
179
|
+
for s in repo_skills:
|
|
180
|
+
n = (s.name or "").lower()
|
|
181
|
+
if n == "claude" and not (
|
|
182
|
+
"anthropic" in family or "claude" in family
|
|
183
|
+
):
|
|
184
|
+
continue
|
|
185
|
+
if n == "gemini" and not (
|
|
186
|
+
"gemini" in family or "google_gemini" in family
|
|
187
|
+
):
|
|
188
|
+
continue
|
|
189
|
+
filtered.append(s)
|
|
190
|
+
repo_skills = filtered
|
|
191
|
+
|
|
159
192
|
logger.debug(f"Triggered {len(repo_skills)} repository skills: {repo_skills}")
|
|
160
193
|
# Build the workspace context information
|
|
161
|
-
|
|
162
|
-
if repo_skills or self.system_message_suffix or
|
|
163
|
-
# TODO(test): add a test for this rendering to make sure they work
|
|
194
|
+
secret_infos = self.get_secret_infos()
|
|
195
|
+
if repo_skills or self.system_message_suffix or secret_infos:
|
|
164
196
|
formatted_text = render_template(
|
|
165
197
|
prompt_dir=str(PROMPT_DIR),
|
|
166
198
|
template_name="system_message_suffix.j2",
|
|
167
199
|
repo_skills=repo_skills,
|
|
168
200
|
system_message_suffix=self.system_message_suffix or "",
|
|
169
|
-
|
|
201
|
+
secret_infos=secret_infos,
|
|
170
202
|
).strip()
|
|
171
203
|
return formatted_text
|
|
172
204
|
elif self.system_message_suffix and self.system_message_suffix.strip():
|
|
@@ -3,6 +3,7 @@ from logging import getLogger
|
|
|
3
3
|
|
|
4
4
|
from openhands.sdk.context.view import View
|
|
5
5
|
from openhands.sdk.event.condenser import Condensation
|
|
6
|
+
from openhands.sdk.llm import LLM
|
|
6
7
|
from openhands.sdk.utils.models import (
|
|
7
8
|
DiscriminatedUnionMixin,
|
|
8
9
|
)
|
|
@@ -28,7 +29,7 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
|
|
|
28
29
|
"""
|
|
29
30
|
|
|
30
31
|
@abstractmethod
|
|
31
|
-
def condense(self, view: View) -> View | Condensation:
|
|
32
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
|
|
32
33
|
"""Condense a sequence of events into a potentially smaller list.
|
|
33
34
|
|
|
34
35
|
New condenser strategies should override this method to implement their own
|
|
@@ -37,6 +38,8 @@ class CondenserBase(DiscriminatedUnionMixin, ABC):
|
|
|
37
38
|
|
|
38
39
|
Args:
|
|
39
40
|
view: A view of the history containing all events that should be condensed.
|
|
41
|
+
agent_llm: LLM instance used by the agent. Condensers use this for token
|
|
42
|
+
counting purposes. Defaults to None.
|
|
40
43
|
|
|
41
44
|
Returns:
|
|
42
45
|
View | Condensation: A condensed view of the events or an event indicating
|
|
@@ -77,18 +80,20 @@ class RollingCondenser(PipelinableCondenserBase, ABC):
|
|
|
77
80
|
"""
|
|
78
81
|
|
|
79
82
|
@abstractmethod
|
|
80
|
-
def should_condense(self, view: View) -> bool:
|
|
83
|
+
def should_condense(self, view: View, agent_llm: LLM | None = None) -> bool:
|
|
81
84
|
"""Determine if a view should be condensed."""
|
|
82
85
|
|
|
83
86
|
@abstractmethod
|
|
84
|
-
def get_condensation(
|
|
87
|
+
def get_condensation(
|
|
88
|
+
self, view: View, agent_llm: LLM | None = None
|
|
89
|
+
) -> Condensation:
|
|
85
90
|
"""Get the condensation from a view."""
|
|
86
91
|
|
|
87
|
-
def condense(self, view: View) -> View | Condensation:
|
|
92
|
+
def condense(self, view: View, agent_llm: LLM | None = None) -> View | Condensation:
|
|
88
93
|
# If we trigger the condenser-specific condensation threshold, compute and
|
|
89
94
|
# return the condensation.
|
|
90
|
-
if self.should_condense(view):
|
|
91
|
-
return self.get_condensation(view)
|
|
95
|
+
if self.should_condense(view, agent_llm=agent_llm):
|
|
96
|
+
return self.get_condensation(view, agent_llm=agent_llm)
|
|
92
97
|
|
|
93
98
|
# Otherwise we're safe to just return the view.
|
|
94
99
|
else:
|