Skip to content

Conversation

@neubig
Copy link
Contributor

@neubig neubig commented Dec 22, 2025

Summary

This PR adds the to_prompt() function for generating XML prompt blocks from skills, following the AgentSkills standard.

Depends on PR #1482 (resource directories support)

View changes from PR #1482

Changes

New to_prompt() Function

Generates XML prompt block for available skills:

from openhands.sdk.context.skills import Skill, to_prompt

skills = [
    Skill(name="pdf-tools", content="...", description="Extract text from PDFs."),
    Skill(name="code-review", content="...", description="Review code for bugs."),
]

prompt = to_prompt(skills)
print(prompt)

Output:

<available_skills>
  <skill name="pdf-tools">Extract text from PDFs.</skill>
  <skill name="code-review">Review code for bugs.</skill>
</available_skills>

Features:

  • Uses description field if available
  • Falls back to first content line (skipping headers)
  • Escapes XML special characters
  • Truncates long descriptions to 200 chars

Backward Compatibility

  • All changes are backward compatible
  • New function is additive

Testing

Added 3 tests covering:

  • XML generation with single and multiple skills
  • XML special character escaping
  • Content fallback when no description

Related Issues

Closes #1478

Part of #1473 (Support AgentSkills standard)


Agent Server images for this PR

GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.12-nodejs22 Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:aaf0950-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-aaf0950-python \
  ghcr.io/openhands/agent-server:aaf0950-python

All tags pushed for this build

ghcr.io/openhands/agent-server:aaf0950-golang-amd64
ghcr.io/openhands/agent-server:aaf0950-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:aaf0950-golang-arm64
ghcr.io/openhands/agent-server:aaf0950-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:aaf0950-java-amd64
ghcr.io/openhands/agent-server:aaf0950-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:aaf0950-java-arm64
ghcr.io/openhands/agent-server:aaf0950-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:aaf0950-python-amd64
ghcr.io/openhands/agent-server:aaf0950-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-amd64
ghcr.io/openhands/agent-server:aaf0950-python-arm64
ghcr.io/openhands/agent-server:aaf0950-nikolaik_s_python-nodejs_tag_python3.12-nodejs22-arm64
ghcr.io/openhands/agent-server:aaf0950-golang
ghcr.io/openhands/agent-server:aaf0950-java
ghcr.io/openhands/agent-server:aaf0950-python

About Multi-Architecture Support

  • Each variant tag (e.g., aaf0950-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., aaf0950-python-amd64) are also available if needed

Add support for AgentSkills standard fields (https://agentskills.io/specification):
- description: Brief description of what the skill does
- license: License under which the skill is distributed
- compatibility: Environment requirements or compatibility notes
- metadata: Arbitrary key-value metadata for extensibility
- allowed_tools: List of pre-approved tools for the skill

Also adds skills-ref as an optional dependency for future validation
and prompt generation utilities.

Closes #1474

Co-authored-by: openhands <openhands@all-hands.dev>
The skills-ref library will be added when validation and prompt
generation utilities are implemented (issue #1478).

Co-authored-by: openhands <openhands@all-hands.dev>
- Add find_skill_md() function to locate SKILL.md files (case-insensitive)
- Add validate_skill_name() function for AgentSkills spec validation
- Update load_skills_from_dir() to support skill-name/SKILL.md directories
- Add directory_name and validate_name parameters to Skill.load()
- Export new functions from __init__.py
- Add 27 unit tests for new functionality

Closes #1475

Co-authored-by: openhands <openhands@all-hands.dev>
- Add find_mcp_config() function to locate .mcp.json files
- Add expand_mcp_variables() for variable expansion (, default)
- Add load_mcp_config() to parse and validate .mcp.json files
- Add mcp_config_path field to Skill model
- Update Skill.load() to auto-load .mcp.json from SKILL.md directories
- .mcp.json takes precedence over mcp_tools frontmatter
- Support  variable expansion
- Export new functions from __init__.py
- Add 19 unit tests for new functionality

Closes #1476

Co-authored-by: openhands <openhands@all-hands.dev>
- Add SkillResources model to track resource directories
- Add discover_skill_resources() function to scan for resources
- Add RESOURCE_DIRECTORIES constant for standard directory names
- Add resources field to Skill model
- Update Skill.load() to auto-discover resources from SKILL.md directories
- Export new classes and functions from __init__.py
- Add 21 unit tests for new functionality

Closes #1477

Co-authored-by: openhands <openhands@all-hands.dev>
- Enhance SkillValidationError with errors list
- Add validate_skill() function for skill directory validation
- Add to_prompt() function for XML prompt generation
- Export new functions from __init__.py
- Add 24 unit tests for new functionality

Closes #1478

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions
Copy link
Contributor

github-actions bot commented Dec 22, 2025

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/context/skills
   skill.py31123424%65, 69–70, 74–75, 79–80, 182–188, 194–198, 204–211, 239, 241–242, 244–245, 247, 258–259, 261–264, 267, 270–272, 277–280, 283–286, 288, 304–306, 309–310, 314, 316–319, 322, 325–327, 329, 355, 358, 366, 371–373, 379, 381–387, 390, 401–402, 413, 430, 432–433, 445–446, 451, 458, 468–469, 471, 477–478, 480, 489–492, 494–497, 505–507, 515–517, 540–541, 543–546, 549–550, 553–555, 558–559, 564–565, 570–571, 575–576, 582, 603–604, 606–609, 611–613, 618–622, 624, 628–629, 631, 634, 652–653, 655–656, 659, 664–666, 669, 671–673, 678–682, 684, 689–690, 694, 697, 736, 738, 740–741, 743–745, 748–751, 754, 756, 759–761, 765–771, 773–774, 776, 779, 802–803, 805–806, 808–810, 813–815, 817–820, 822–825, 828, 831–833, 835–840, 843–848
TOTAL14594695352% 

Reduce test code by 74% while maintaining essential coverage:
- test_agentskills_fields.py: 299 → 73 lines
- test_skill_md_convention.py: 405 → 86 lines
- test_mcp_json_config.py: 301 → 89 lines
- test_resource_directories.py: 300 → 83 lines
- test_validation_prompt.py: 332 → 101 lines

Total: 1637 → 432 lines

Co-authored-by: openhands <openhands@all-hands.dev>
Reduce test code while maintaining essential coverage.

Co-authored-by: openhands <openhands@all-hands.dev>
Reduce test code while maintaining essential coverage.

Co-authored-by: openhands <openhands@all-hands.dev>
Resolved merge conflicts in:
- openhands-sdk/openhands/sdk/context/skills/skill.py
- tests/sdk/context/skill/test_agentskills_fields.py

The resolution keeps both:
1. Pydantic field validators for allowed_tools and metadata from main
2. Skill name validation logic from this branch
3. SKILL.md convention support from this branch

Co-authored-by: openhands <openhands@all-hands.dev>
Extract helper functions to simplify the load_skills_from_dir function:

- _find_third_party_files: Find .cursorrules, AGENTS.md, etc. in repo root
- _find_skill_md_directories: Find AgentSkills-style SKILL.md directories
- _find_regular_md_files: Find regular .md files excluding SKILL.md dirs
- _load_skill_safe: Load skills with consistent error handling

This improves code readability and maintainability by following the
single responsibility principle. Each helper function handles one
specific aspect of skill discovery or loading.

Co-authored-by: openhands <openhands@all-hands.dev>
- Merged field validators from main (_parse_allowed_tools, _convert_metadata_values)
- Kept new fields (mcp_config_path, resources) from the PR branch
- Updated tests to use Pydantic ValidationError for description validation
- Removed redundant _parse_agentskills_fields method (functionality moved to validators)

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Dec 22, 2025

Looks like there are a few issues preventing this PR from being merged!

  • GitHub Actions are failing:
    • Agent Server

If you'd like me to help, just leave a comment, like

@OpenHands please fix the failing actions on PR #1483 at branch `feat/validation-prompt-utils`

Feel free to include any additional details that might help me get this PR into a better state.

You can manage your notification settings

openhands-agent and others added 10 commits December 28, 2025 13:09
Co-authored-by: openhands <openhands@all-hands.dev>
- Remove redundant tuple element from _find_skill_md_directories
  (directory_path can be derived from skill_md.parent)
- Replace _load_skill_safe wrapper with _load_and_categorize that
  combines loading and categorization in one function
- Auto-validate skill name when directory_name is provided
  (removed separate validate_name parameter)
- Fix case-insensitive search for third-party files to iterate
  over all files instead of checking specific variants
- Update tests to check for specific error messages instead of
  magic number assertions

Co-authored-by: openhands <openhands@all-hands.dev>
SKILL.md directories should always be categorized as knowledge_skills
(progressive loading), not repo_skills (permanent context), even when
they have no triggers defined.

This addresses enyst's feedback that AgentSkills are fundamentally
different from permanent OH skills like repo.md - they should use
progressive loading while permanent skills should use AGENTS.md.

Changes:
- Modified _load_and_categorize to always put SKILL.md files in
  knowledge_skills when directory_name is provided
- Added test_skill_md_always_knowledge_skill to verify the behavior

Co-authored-by: openhands <openhands@all-hands.dev>
The file_content parameter was only used in unit tests. Tests have been
updated to use pytest's tmp_path fixture and write content to actual
temp files instead.

Co-authored-by: openhands <openhands@all-hands.dev>
…gacy formats

Split load() into:
- _load_agentskills_skill(): For SKILL.md files (AgentSkills format)
- _load_legacy_openhands_skill(): For legacy OpenHands skill files
- _create_skill_from_metadata(): Shared helper for Skill object creation

Co-authored-by: openhands <openhands@all-hands.dev>
- load_skills_from_dir() now returns 3 dictionaries: repo_skills, knowledge_skills, agent_skills
- AgentSkills (SKILL.md directories) are categorized into agent_skills (separate from OpenHands skills)
- Updated all callers to handle the new return type
- Updated tests to verify the new categorization

This addresses enyst's review comment about keeping AgentSkills separate from
OpenHands skills, as they follow different standards and loading patterns.

Co-authored-by: openhands <openhands@all-hands.dev>
Add support for .mcp.json files in AgentSkills directories (SKILL.md format),
following the AgentSkills standard for MCP server configuration.

Changes:
- Add _find_mcp_config() to locate .mcp.json files in skill directories
- Add _expand_mcp_variables() for variable expansion (${VAR}, ${VAR:-default})
- Add _load_mcp_config() to load and validate .mcp.json files
- Update _load_agentskills_skill() to load .mcp.json (agent_skills only)
- Update _load_legacy_openhands_skill() to load mcp_tools from frontmatter

MCP loading rules:
- AgentSkills (SKILL.md): ONLY use .mcp.json, ignore mcp_tools frontmatter
- Legacy skills (.md): ONLY use mcp_tools frontmatter, no .mcp.json support

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
Copy link
Contributor Author

neubig commented Dec 31, 2025

@OpenHands resolve merge conflicts with main

@openhands-ai
Copy link

openhands-ai bot commented Dec 31, 2025

I'm on it! neubig can track my progress at all-hands.dev

Resolved merge conflicts:
- openhands-sdk/openhands/sdk/context/skills/skill.py: Integrated resource
  directories and MCP config support with main's refactored Skill.load() API
- tests/sdk/context/skill/test_agentskills_fields.py: Used main's test structure
- tests/sdk/context/skill/test_skill_md_convention.py: Updated imports to use
  public functions (find_skill_md, validate_skill_name)
- tests/sdk/context/skill/test_mcp_json_config.py: Updated to use new API
- tests/sdk/context/skill/test_resource_directories.py: Updated to use new API

Co-authored-by: openhands <openhands@all-hands.dev>
Merged changes from main branch (fd98c16) which refactored Skill.load()
to use helper methods (_load_agentskills_skill, _load_legacy_openhands_skill,
_create_skill_from_metadata) and added agent_skills as a third category.

Key changes:
- Integrated validation and prompt utilities with main's refactored structure
- Made find_skill_md and validate_skill_name public functions (exported from __init__.py)
- Added mcp_config_path and resources fields to Skill model
- Updated _load_agentskills_skill to load MCP config and resources
- Updated _create_skill_from_metadata to accept mcp_tools, mcp_config_path, resources
- Updated tests to use new API (removed directory_name parameter)

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Dec 31, 2025

Summary: Merge Conflicts Resolved

I successfully resolved the merge conflicts between the feat/validation-prompt-utils branch and origin/main.

Conflicts Resolved

  1. openhands-sdk/openhands/sdk/context/skills/skill.py - Main branch had refactored Skill.load() into helper methods (_load_agentskills_skill, _load_legacy_openhands_skill, _create_skill_from_metadata) and added agent_skills as a third category in load_skills_from_dir().

  2. tests/sdk/context/skill/test_skill_md_convention.py - Updated imports to use public functions.

Key Integration Changes

  • Made utility functions public: find_skill_md() and validate_skill_name() are now exported from __init__.py (previously private _find_skill_md and _validate_skill_name)
  • Added new Skill model fields: mcp_config_path and resources for AgentSkills support
  • Updated _load_agentskills_skill(): Now loads MCP config from .mcp.json and discovers resource directories
  • Updated _create_skill_from_metadata(): Accepts mcp_tools, mcp_config_path, and resources parameters
  • Fixed tests: Removed obsolete directory_name parameter from test calls (main branch auto-detects SKILL.md files)

Verification

  • ✅ All 88 tests in tests/sdk/context/skill/ pass
  • ✅ Pre-commit hooks pass (ruff format, ruff lint, pycodestyle, pyright)
  • ✅ Changes pushed to origin/feat/validation-prompt-utils

The PR #1483 is now updated and ready for review.

View full conversation

Resolved merge conflicts by keeping the .mcp.json loading functionality
from this branch while incorporating main's changes (PR #1480).

Key changes preserved:
- AgentSkills (SKILL.md) load .mcp.json with variable expansion
- Legacy skills load mcp_tools from frontmatter only
- _find_mcp_config, _expand_mcp_variables, _load_mcp_config functions

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 5, 2026

I'm on it! neubig can track my progress at all-hands.dev

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 5, 2026

Summary

I've successfully completed all tasks for PR #1483:

✅ Checklist

  1. Brought PR up to date with PR feat: support resource directories (scripts/, references/, assets/) #1482 - Merged origin/feat/resource-directories into feat/validation-prompt-utils, resolving all conflicts
  2. Verified diff is minimal and focused - Changes are only related to validation and prompt generation utilities:
    • exceptions.py: Enhanced SkillValidationError with errors list (+12 lines)
    • skill.py: Added validate_skill(), to_prompt(), and _escape_xml() functions (+134 lines)
    • __init__.py: Added exports for new functions (+6 lines)
    • test_validation_prompt.py: New test file (+103 lines)
    • Minor import adjustments in 2 test files (-2 lines)
  3. Ran tests and pre-commit - All 90 skill tests pass, pre-commit passes
  4. Pushed and monitored CI - All 21 CI checks passed ✅

Changes Summary

The PR adds validation and prompt generation utilities for skills:

  • validate_skill(): Validates skill directories according to AgentSkills spec
  • to_prompt(): Generates XML prompt block for available skills
  • Enhanced SkillValidationError: Now includes a list of specific errors

CI Status

All checks passed:

  • sdk-tests ✅
  • tools-tests ✅
  • cross-tests ✅
  • agent-server-tests ✅
  • pre-commit ✅
  • All build & push jobs ✅
  • coverage-report ✅

PR link: #1483

View full conversation

YAGNI - validation is already handled by Skill.load() and Pydantic validators.
Keep only to_prompt() for XML prompt generation.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig changed the title feat: add validation and prompt generation utilities feat: add prompt generation utilities Jan 5, 2026
Remove SkillValidationError enhancement and validate_skill_name export.
These will be part of a separate PR for Pydantic validation improvements.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig changed the title feat: add prompt generation utilities feat: add to_prompt() for XML skill prompt generation Jan 5, 2026
@neubig neubig marked this pull request as ready for review January 5, 2026 22:37
@neubig neubig requested review from enyst and xingyaoww January 5, 2026 22:40
Co-authored-by: openhands <openhands@all-hands.dev>
@neubig
Copy link
Contributor Author

neubig commented Jan 6, 2026

@OpenHands reflect xingyaoww's two comments

@openhands-ai
Copy link

openhands-ai bot commented Jan 6, 2026

I'm on it! neubig can track my progress at all-hands.dev

- Show 'no available skills' message when skills list is empty
- Add truncation indicator with character count when description exceeds limit
- Include source file path in truncation message for reference
- Add max_description_length parameter for configurability

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 6, 2026

Summary

I've addressed xingyaoww's two review comments on PR #1483:

Changes Made

1. Empty skills message (Comment on line 801)

  • Changed the empty skills output from <available_skills>\n</available_skills> to <available_skills>\n no available skills\n</available_skills>

2. Truncation indicator with source reference (Comment on line 813)

  • Added ... after truncated descriptions
  • Added [X characters truncated] message showing how many characters were cut
  • Added View {source} for complete information when the skill has a source path
  • Made max_description_length a configurable parameter (default 200)

Example Output

For a skill with a 250-character description and source path:

<skill name="test">AAAA... [50 characters truncated. View /path/to/skill.md for complete information]</skill>

Files Changed

  • openhands-sdk/openhands/sdk/context/skills/skill.py - Updated to_prompt() function
  • tests/sdk/context/skill/test_validation_prompt.py - Added 2 new tests for truncation behavior

Verification

  • ✅ All pre-commit checks pass
  • ✅ All 5 tests pass
  • ✅ Changes pushed to origin/feat/validation-prompt-utils

View full conversation

@neubig neubig requested a review from xingyaoww January 6, 2026 13:47
@neubig
Copy link
Contributor Author

neubig commented Jan 6, 2026

@OpenHands reflect xingyaoww's most recent comments.

@neubig neubig marked this pull request as draft January 6, 2026 17:22
@openhands-ai
Copy link

openhands-ai bot commented Jan 6, 2026

I'm on it! neubig can track my progress at all-hands.dev

…truncated

Address xingyaoww's review comments:
1. Use Python standard library xml.sax.saxutils.escape for XML escaping
   instead of custom implementation
2. Count content after the description line as truncated characters
   when using content fallback (not just when description exceeds max length)

Co-authored-by: openhands <openhands@all-hands.dev>
@openhands-ai
Copy link

openhands-ai bot commented Jan 6, 2026

Summary

I addressed xingyaoww's most recent review comments on PR #1483:

Changes Made

  1. Use Python standard library for XML escaping (comment at line 844)

    • Replaced the custom _escape_xml() implementation with xml.sax.saxutils.escape from the Python standard library
    • The function now uses xml_escape(text, entities={'"': "&quot;", "'": "&apos;"}) which produces identical output
  2. Count remaining content as truncated characters (comment at line 814)

    • When using content fallback (no description provided), the code now counts all content after the first non-header line as truncated characters
    • This means if a skill has content like "# Header\n\nDescription line.\n\nMore content...", the "More content..." portion is counted in the truncation message
    • Added a new test test_to_prompt_content_fallback_counts_remaining_as_truncated to verify this behavior

Files Changed

  • openhands-sdk/openhands/sdk/context/skills/skill.py - Updated to_prompt() and _escape_xml() functions
  • tests/sdk/context/skill/test_validation_prompt.py - Added new test for content truncation behavior

Verification

  • All 6 tests pass
  • Pre-commit hooks pass (ruff format, ruff lint, pycodestyle, pyright)

The changes have been pushed to origin/feat/validation-prompt-utils.

View full conversation

Remove trivial wrapper function and use xml_escape directly.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig marked this pull request as ready for review January 6, 2026 17:34
@neubig neubig requested a review from xingyaoww January 6, 2026 17:34
Comment on lines +811 to +824
# Extract first non-empty, non-header line from content as fallback
# Track position to calculate truncated content after the description
chars_before_desc = 0
for line in skill.content.split("\n"):
stripped = line.strip()
# Skip markdown headers and empty lines
if not stripped or stripped.startswith("#"):
chars_before_desc += len(line) + 1 # +1 for newline
continue
description = stripped
# Calculate remaining content after this line as truncated
desc_end_pos = chars_before_desc + len(line)
content_truncated = max(0, len(skill.content) - desc_end_pos)
break
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@OpenHands /codereview /codereview-roasted

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm on it! xingyaoww can track my progress at all-hands.dev

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

I provided a Linus Torvalds-style code review for PR #1483 (feat: add to_prompt() for XML skill prompt generation) as requested with the /codereview-roasted command.

Review Verdict: 🟡 Acceptable - Worth merging

Key Findings:

  1. Core logic is sound - The XML generation works correctly and uses the standard library (xml.sax.saxutils.escape) for escaping, which is good.

  2. Main improvement opportunity (Lines 809-824): The manual character position tracking for content fallback is fragile and over-engineered. The code tracks chars_before_desc, desc_end_pos, and content_truncated through manual byte counting - this is error-prone and could be simplified to just detect "has more content" rather than counting exact characters.

  3. Over-engineering concern (Lines 827-840): The exact truncation character counts (e.g., "47 characters truncated") solve an imaginary problem. Users don't care about exact counts - they just need to know there's more content and where to find it.

Suggested simplification: Replace the character-counting logic with a simpler "are there more meaningful lines?" check, and simplify the truncation message to something like "... [see /skills/test.md for full content]".

No code changes were made - this was a review-only request. The feedback has been provided above for the PR author to consider.

View full conversation

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xingyaoww Would you like to make this change? Or keep as is? I kinda feel as-is might be OK

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add validation and prompt generation utilities

4 participants