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.
Files changed (32) hide show
  1. openhands/sdk/__init__.py +2 -0
  2. openhands/sdk/agent/agent.py +27 -0
  3. openhands/sdk/agent/base.py +88 -82
  4. openhands/sdk/agent/prompts/system_prompt.j2 +1 -1
  5. openhands/sdk/agent/utils.py +3 -0
  6. openhands/sdk/context/agent_context.py +45 -3
  7. openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +4 -0
  8. openhands/sdk/context/prompts/templates/system_message_suffix.j2 +9 -0
  9. openhands/sdk/context/skills/__init__.py +12 -0
  10. openhands/sdk/context/skills/skill.py +275 -296
  11. openhands/sdk/context/skills/types.py +4 -0
  12. openhands/sdk/context/skills/utils.py +442 -0
  13. openhands/sdk/conversation/impl/local_conversation.py +42 -14
  14. openhands/sdk/conversation/state.py +52 -20
  15. openhands/sdk/event/llm_convertible/action.py +20 -0
  16. openhands/sdk/git/utils.py +31 -6
  17. openhands/sdk/hooks/conversation_hooks.py +57 -10
  18. openhands/sdk/llm/llm.py +58 -74
  19. openhands/sdk/llm/router/base.py +12 -0
  20. openhands/sdk/llm/utils/telemetry.py +2 -2
  21. openhands/sdk/plugin/__init__.py +22 -0
  22. openhands/sdk/plugin/plugin.py +299 -0
  23. openhands/sdk/plugin/types.py +226 -0
  24. openhands/sdk/tool/__init__.py +7 -1
  25. openhands/sdk/tool/builtins/__init__.py +4 -0
  26. openhands/sdk/tool/tool.py +60 -9
  27. openhands/sdk/workspace/remote/async_remote_workspace.py +16 -0
  28. openhands/sdk/workspace/remote/base.py +16 -0
  29. {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/METADATA +1 -1
  30. {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/RECORD +32 -28
  31. {openhands_sdk-1.7.4.dist-info → openhands_sdk-1.8.1.dist-info}/WHEEL +0 -0
  32. {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",
@@ -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
@@ -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 BaseModel, ConfigDict, Field, PrivateAttr
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, LLMSummarizingCondenser
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 BUILT_IN_TOOLS, Tool, ToolDefinition, resolve_tool
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": "repo.md",
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._tools:
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
- # Always include built-in tools; not subject to filtering
259
- # Instantiate built-in tools using their .create() method
260
- for tool_class in BUILT_IN_TOOLS:
261
- tools.extend(tool_class.create(state))
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 resolve_diff_from_deserialized(
333
+ def verify(
304
334
  self,
305
335
  persisted: "AgentBase",
306
336
  events: "Sequence[Any] | None" = None,
307
337
  ) -> "AgentBase":
308
- """
309
- Return a new AgentBase instance equivalent to `persisted` but with
310
- explicitly whitelisted fields (e.g. api_key) taken from `self`.
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 persisted agent from the conversation state.
314
- events: Optional event sequence to scan for used tools if tool
315
- names don't match. Only scanned when needed (O(n) fallback).
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
- f"Cannot resolve from deserialized: persisted agent is of type "
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
- # Tools unchanged, proceed normally
357
- pass
358
- elif events is not None:
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
- f"Cannot resume conversation: tools that were used in history "
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
- if self_dump != reconciled_dump:
401
- raise ValueError(
402
- "The Agent provided is different from the one in persisted state.\n"
403
- f"Diff: {pretty_pydantic_diff(self, reconciled)}"
404
- )
405
- return reconciled
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._tools:
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 `.openhands/skills/repo.md` under the repository root as your persistent memory for repository-specific knowledge and context.
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
@@ -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
- repo_skills = [s for s in self.skills if s.trigger is None]
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"Triggered {len(repo_skills)} repository skills: {repo_skills}")
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
- if repo_skills or self.system_message_suffix or secret_infos:
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
  ]