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.
Files changed (56) hide show
  1. openhands/sdk/__init__.py +9 -1
  2. openhands/sdk/agent/agent.py +35 -12
  3. openhands/sdk/agent/base.py +53 -7
  4. openhands/sdk/agent/prompts/model_specific/anthropic_claude.j2 +3 -0
  5. openhands/sdk/agent/prompts/model_specific/google_gemini.j2 +1 -0
  6. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5-codex.j2 +2 -0
  7. openhands/sdk/agent/prompts/model_specific/openai_gpt/gpt-5.j2 +3 -0
  8. openhands/sdk/agent/prompts/self_documentation.j2 +15 -0
  9. openhands/sdk/agent/prompts/system_prompt.j2 +29 -1
  10. openhands/sdk/agent/utils.py +18 -4
  11. openhands/sdk/context/__init__.py +2 -0
  12. openhands/sdk/context/agent_context.py +42 -10
  13. openhands/sdk/context/condenser/base.py +11 -6
  14. openhands/sdk/context/condenser/llm_summarizing_condenser.py +169 -20
  15. openhands/sdk/context/condenser/no_op_condenser.py +2 -1
  16. openhands/sdk/context/condenser/pipeline_condenser.py +10 -9
  17. openhands/sdk/context/condenser/utils.py +149 -0
  18. openhands/sdk/context/prompts/prompt.py +40 -2
  19. openhands/sdk/context/prompts/templates/system_message_suffix.j2 +3 -3
  20. openhands/sdk/context/skills/__init__.py +2 -0
  21. openhands/sdk/context/skills/skill.py +152 -1
  22. openhands/sdk/context/view.py +287 -27
  23. openhands/sdk/conversation/base.py +17 -0
  24. openhands/sdk/conversation/conversation.py +19 -0
  25. openhands/sdk/conversation/exceptions.py +29 -4
  26. openhands/sdk/conversation/impl/local_conversation.py +126 -9
  27. openhands/sdk/conversation/impl/remote_conversation.py +152 -3
  28. openhands/sdk/conversation/state.py +42 -1
  29. openhands/sdk/conversation/stuck_detector.py +81 -45
  30. openhands/sdk/conversation/types.py +30 -0
  31. openhands/sdk/event/llm_convertible/system.py +16 -20
  32. openhands/sdk/hooks/__init__.py +30 -0
  33. openhands/sdk/hooks/config.py +180 -0
  34. openhands/sdk/hooks/conversation_hooks.py +227 -0
  35. openhands/sdk/hooks/executor.py +155 -0
  36. openhands/sdk/hooks/manager.py +170 -0
  37. openhands/sdk/hooks/types.py +40 -0
  38. openhands/sdk/io/cache.py +85 -0
  39. openhands/sdk/io/local.py +39 -2
  40. openhands/sdk/llm/llm.py +3 -2
  41. openhands/sdk/llm/message.py +4 -3
  42. openhands/sdk/llm/mixins/fn_call_converter.py +61 -16
  43. openhands/sdk/llm/mixins/non_native_fc.py +5 -1
  44. openhands/sdk/llm/utils/model_features.py +64 -24
  45. openhands/sdk/llm/utils/model_prompt_spec.py +98 -0
  46. openhands/sdk/llm/utils/verified_models.py +6 -4
  47. openhands/sdk/logger/logger.py +1 -1
  48. openhands/sdk/tool/schema.py +10 -0
  49. openhands/sdk/tool/tool.py +2 -2
  50. openhands/sdk/utils/async_executor.py +76 -67
  51. openhands/sdk/utils/models.py +1 -1
  52. openhands/sdk/utils/paging.py +63 -0
  53. {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/METADATA +3 -3
  54. {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/RECORD +56 -41
  55. {openhands_sdk-1.5.0.dist-info → openhands_sdk-1.7.2.dist-info}/WHEEL +0 -0
  56. {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 AgentContext
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
  ]
@@ -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
- # Always expose a 'security_risk' parameter in tool schemas.
113
- # This ensures the schema remains consistent, even if the
114
- # security analyzer is disabled. Validation of this field
115
- # happens dynamically at runtime depending on the analyzer
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(
@@ -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
- # Add MCP tools if configured
203
- if self.mcp_config:
204
- mcp_tools = create_mcp_tools(self.mcp_config, timeout=30)
205
- tools.extend(mcp_tools)
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 'security_policy.j2' %}
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 %}
@@ -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
- state: The conversation state containing events
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
- on_event: Optional callback for handling condensation events
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 get_secret_names(self) -> list[str]:
140
- """Get the list of secret names from the secrets field.
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 secret names. Returns an empty list if no secrets are configured.
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
- return list(self.secrets.keys())
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(self) -> str | None:
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
- secret_names = self.get_secret_names()
162
- if repo_skills or self.system_message_suffix or secret_names:
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
- secret_names=secret_names,
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(self, view: View) -> 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: