From b3316c70222989d2d5713ed09fb0b6d8d3687139 Mon Sep 17 00:00:00 2001 From: openhands Date: Wed, 4 Mar 2026 20:00:01 +0000 Subject: [PATCH 1/6] docs: add mixed marketplace skills guide Add documentation for combining local and remote skills from OpenHands extensions repository. Includes: - Use case explanation - Loading pattern comparison - Full example code reference - Local skill creation guide - Precedence rules Co-authored-by: openhands --- sdk/guides/mixed-marketplace-skills.mdx | 262 ++++++++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 sdk/guides/mixed-marketplace-skills.mdx diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx new file mode 100644 index 00000000..8c428012 --- /dev/null +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -0,0 +1,262 @@ +--- +title: Mixed Marketplace Skills +description: Combine local skills with remote skills from OpenHands extensions to create custom skill configurations. +--- + +import RunExampleCode from "/sdk/shared-snippets/how-to-run-example.mdx"; + +This guide shows how to combine locally-defined skills with remote skills from the [OpenHands extensions repository](https://github.com/OpenHands/extensions). + +## Use Case + +Teams often need to: +- Maintain custom workflow-specific skills locally +- Leverage community skills from OpenHands extensions +- Create curated skill sets for specific projects + +## Loading Pattern + +The SDK provides three approaches for loading skills: + +| Method | Source | Use Case | +|--------|--------|----------| +| `load_skills_from_dir()` | Local directory | Project-specific skills | +| `load_public_skills()` | OpenHands/extensions | Community skills | +| `load_available_skills()` | Multiple sources | Combined skill loading | + +## Example: Combining Local and Remote Skills + + +Full example: [examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py) + + +```python icon="python" expandable examples/01_standalone_sdk/43_mixed_marketplace_skills/main.py +"""Example: Mixed Marketplace Skills - Combining Local and Remote Skills + +This example demonstrates how to create a marketplace that combines: +1. Local skills hosted in your project directory +2. Remote skills from the OpenHands/extensions repository + +Use Case: +--------- +Teams often need to maintain their own custom skills while also leveraging +the community skills from OpenHands. This pattern allows you to: +- Keep specialized/private skills in your repository +- Reference public skills from OpenHands/extensions +- Create a curated skill set tailored for your workflow + +Directory Structure: +------------------- +43_mixed_marketplace_skills/ +├── .plugin/ +│ └── marketplace.json # Marketplace configuration +├── local_skills/ +│ └── greeting-helper/ +│ └── SKILL.md # Local skill following AgentSkills standard +├── main.py # This example script +└── README.md + +The marketplace.json can define: +- Local skills to include +- Remote skills to pull from OpenHands/extensions +- Skill dependencies between local and remote skills +""" + +import argparse +import os +from pathlib import Path + +from pydantic import SecretStr + +from openhands.sdk import LLM, Agent, AgentContext, Conversation +from openhands.sdk.context.skills import ( + load_public_skills, + load_skills_from_dir, +) +from openhands.sdk.tool import Tool +from openhands.tools.file_editor import FileEditorTool +from openhands.tools.terminal import TerminalTool + + +def main(): + parser = argparse.ArgumentParser(description="Mixed Marketplace Skills Example") + parser.add_argument( + "--dry-run", + action="store_true", + help="Run without LLM (just show skill loading)", + ) + args = parser.parse_args() + + # ========================================================================= + # Part 1: Loading Local Skills from Directory + # ========================================================================= + print("=" * 80) + print("Part 1: Loading Local Skills from Directory") + print("=" * 80) + + script_dir = Path(__file__).parent + local_skills_dir = script_dir / "local_skills" + + print(f"\nLoading local skills from: {local_skills_dir}") + + # Load skills from the local directory + # This loads any SKILL.md files following the AgentSkills standard + repo_skills, knowledge_skills, local_skills = load_skills_from_dir(local_skills_dir) + + print("\nLoaded local skills:") + for name, skill in local_skills.items(): + print(f" - {name}: {skill.description or 'No description'}") + if skill.trigger: + # KeywordTrigger has 'keywords', TaskTrigger has 'triggers' + trigger_values = getattr(skill.trigger, "keywords", None) or getattr( + skill.trigger, "triggers", None + ) + if trigger_values: + print(f" Triggers: {trigger_values}") + + # ========================================================================= + # Part 2: Loading Remote Skills from OpenHands/extensions + # ========================================================================= + print("\n" + "=" * 80) + print("Part 2: Loading Remote Skills from OpenHands/extensions") + print("=" * 80) + + print("\nLoading public skills from https://github.com/OpenHands/extensions...") + + # Load public skills from the OpenHands extensions repository + # This pulls from the default marketplace at OpenHands/extensions + public_skills = load_public_skills() + + print(f"\nLoaded {len(public_skills)} public skills from OpenHands/extensions:") + for skill in public_skills[:5]: # Show first 5 + desc = (skill.description or "No description")[:50] + print(f" - {skill.name}: {desc}...") + if len(public_skills) > 5: + print(f" ... and {len(public_skills) - 5} more") + + # ========================================================================= + # Part 3: Combining Local and Remote Skills + # ========================================================================= + print("\n" + "=" * 80) + print("Part 3: Combining Local and Remote Skills") + print("=" * 80) + + # Combine skills with local skills taking precedence + # This allows local skills to override public skills with the same name + combined_skills = list(local_skills.values()) + public_skills + + # Remove duplicates (keep first occurrence = local takes precedence) + seen_names: set[str] = set() + unique_skills = [] + for skill in combined_skills: + if skill.name not in seen_names: + seen_names.add(skill.name) + unique_skills.append(skill) + + print(f"\nTotal combined skills: {len(unique_skills)}") + print(f" - Local skills: {len(local_skills)}") + print(f" - Public skills: {len(public_skills)}") + + local_names = list(local_skills.keys()) + public_names = [s.name for s in public_skills[:5]] + print(f"\nSkills by source:") + print(f" Local: {local_names}") + print(f" Remote (first 5): {public_names}") + + # ========================================================================= + # Part 4: Using Skills with an Agent + # ========================================================================= + print("\n" + "=" * 80) + print("Part 4: Using Skills with an Agent") + print("=" * 80) + + api_key = os.getenv("LLM_API_KEY") + model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929") + + if args.dry_run or not api_key: + print("\nSkipping agent demo (LLM_API_KEY not set)") + print("To run the full demo, set the LLM_API_KEY environment variable:") + print(" export LLM_API_KEY=your-api-key") + return + + print(f"\nUsing model: {model}") + + llm = LLM( + usage_id="mixed-skills-demo", + model=model, + api_key=SecretStr(api_key), + ) + + tools = [ + Tool(name=TerminalTool.name), + Tool(name=FileEditorTool.name), + ] + + # Create agent context with combined skills + agent_context = AgentContext(skills=unique_skills) + + agent = Agent(llm=llm, tools=tools, agent_context=agent_context) + conversation = Conversation(agent=agent, workspace=str(script_dir)) + + # Test the agent with a query that should trigger both local and public skills + print("\nSending message to trigger skills...") + conversation.send_message( + "Tell me about GitHub best practices. " + "Also, can you give me a creative greeting?" + ) + conversation.run() + + print(f"\nTotal cost: ${llm.metrics.accumulated_cost:.4f}") + print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}") + + +if __name__ == "__main__": + main() +``` + + + +## Creating a Local Skill + +Local skills follow the [AgentSkills standard](https://agentskills.io/specification). Create a `SKILL.md` file: + +```markdown icon="markdown" +--- +name: greeting-helper +description: A local skill that helps generate creative greetings. +triggers: + - greeting + - hello + - salutation +--- + +# Greeting Helper Skill + +When asked for a greeting, provide creative options in different styles: + +1. **Formal**: "Good day, esteemed colleague" +2. **Casual**: "Hey there!" +3. **Enthusiastic**: "Hello, wonderful human!" +``` + +## Skill Precedence + +When combining skills, local skills take precedence over public skills with the same name: + +```python icon="python" +# Local skills override public skills with matching names +combined_skills = list(local_skills.values()) + public_skills + +seen_names = set() +unique_skills = [] +for skill in combined_skills: + if skill.name not in seen_names: + seen_names.add(skill.name) + unique_skills.append(skill) +``` + +## Next Steps + +- **[Skills Overview](/overview/skills)** - Learn more about skill types +- **[Public Skills](/sdk/guides/skill#loading-public-skills)** - Load from OpenHands extensions +- **[Custom Tools](/sdk/guides/custom-tools)** - Create specialized tools From 32e23dbadda76d7646335427636d3613f540edee Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 5 Mar 2026 13:05:48 +0000 Subject: [PATCH 2/6] docs: clarify marketplace skill filtering Co-authored-by: openhands --- sdk/guides/mixed-marketplace-skills.mdx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx index 8c428012..b87bde88 100644 --- a/sdk/guides/mixed-marketplace-skills.mdx +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -24,6 +24,15 @@ The SDK provides three approaches for loading skills: | `load_public_skills()` | OpenHands/extensions | Community skills | | `load_available_skills()` | Multiple sources | Combined skill loading | +## Marketplace format note + +OpenHands reads `marketplace.json` using the Claude Code plugin marketplace schema, +plus an OpenHands extension (`skills[]`) for listing skills directly. When loading +public skills, `skills[]` entries are treated as direct skill sources and +`plugins[]` entries are treated as pointers to skill directories (not full plugin +bundles). Local skills still live in `local_skills/` and are merged separately. + + ## Example: Combining Local and Remote Skills @@ -56,10 +65,9 @@ Directory Structure: ├── main.py # This example script └── README.md -The marketplace.json can define: -- Local skills to include -- Remote skills to pull from OpenHands/extensions -- Skill dependencies between local and remote skills +The marketplace.json lists which remote skills to include. In OpenHands, entries +in `skills[]` or `plugins[]` should point directly to skill directories containing +`SKILL.md`; local skills live in `local_skills/` and are loaded separately. """ import argparse From a5a46092581c774a25788aa3dd6eb8fb5a1b3e19 Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 5 Mar 2026 14:01:24 +0000 Subject: [PATCH 3/6] fix: update docs to reference skills/ directory The example was updated to use skills/ instead of local_skills/ to follow the standard plugin structure. Co-authored-by: openhands --- sdk/guides/mixed-marketplace-skills.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx index b87bde88..f0c07f4f 100644 --- a/sdk/guides/mixed-marketplace-skills.mdx +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -30,7 +30,7 @@ OpenHands reads `marketplace.json` using the Claude Code plugin marketplace sche plus an OpenHands extension (`skills[]`) for listing skills directly. When loading public skills, `skills[]` entries are treated as direct skill sources and `plugins[]` entries are treated as pointers to skill directories (not full plugin -bundles). Local skills still live in `local_skills/` and are merged separately. +bundles). Local skills live in `skills/` and are merged separately. ## Example: Combining Local and Remote Skills From 6ecfe0c9047728e8386f1ff4ba58fdf71d389f72 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 7 Mar 2026 01:59:11 +0000 Subject: [PATCH 4/6] docs: clarify marketplace schema is filter list for public skills Address review feedback: Clarify that marketplace.json plugin entry names are used as a filter list for loading public skills, not defining skills directly. Local skills are merged separately from local_skills/ directory. Co-authored-by: openhands --- sdk/guides/mixed-marketplace-skills.mdx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx index f0c07f4f..b09dc00e 100644 --- a/sdk/guides/mixed-marketplace-skills.mdx +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -26,11 +26,11 @@ The SDK provides three approaches for loading skills: ## Marketplace format note -OpenHands reads `marketplace.json` using the Claude Code plugin marketplace schema, -plus an OpenHands extension (`skills[]`) for listing skills directly. When loading -public skills, `skills[]` entries are treated as direct skill sources and -`plugins[]` entries are treated as pointers to skill directories (not full plugin -bundles). Local skills live in `skills/` and are merged separately. +The `marketplace.json` follows the Claude Code plugin marketplace schema. In OpenHands, the +plugin entry names are used as a filter list for which public skills to load; local +skills live in `local_skills/` and are merged separately. + +Additionally, OpenHands extends the schema with an optional `skills[]` array for listing skills directly (these are treated as direct skill sources, not plugin bundles). ## Example: Combining Local and Remote Skills From 8607df1fed4ff6e7b683d54c5e97bcba51dcbb68 Mon Sep 17 00:00:00 2001 From: openhands Date: Sat, 7 Mar 2026 20:33:31 +0000 Subject: [PATCH 5/6] Clarify mixed marketplace guide flow --- sdk/guides/mixed-marketplace-skills.mdx | 27 ++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx index b09dc00e..c4fd54fe 100644 --- a/sdk/guides/mixed-marketplace-skills.mdx +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -16,22 +16,35 @@ Teams often need to: ## Loading Pattern -The SDK provides three approaches for loading skills: +This guide focuses on the two loader APIs used in the example: | Method | Source | Use Case | |--------|--------|----------| | `load_skills_from_dir()` | Local directory | Project-specific skills | -| `load_public_skills()` | OpenHands/extensions | Community skills | -| `load_available_skills()` | Multiple sources | Combined skill loading | +| `load_public_skills()` | OpenHands/extensions | Community skills filtered by a marketplace | + +## Example repository layout + +The example repository separates local skills from the marketplace configuration that filters public skills: + +```text +43_mixed_marketplace_skills/ +├── .plugin/ +│ └── marketplace.json +├── local_skills/ +│ └── greeting-helper/ +│ └── SKILL.md +├── main.py +└── README.md +``` ## Marketplace format note -The `marketplace.json` follows the Claude Code plugin marketplace schema. In OpenHands, the -plugin entry names are used as a filter list for which public skills to load; local -skills live in `local_skills/` and are merged separately. +The `.plugin/marketplace.json` file follows the Claude Code plugin marketplace schema. In OpenHands, plugin entry names are used as a filter list for which public skills to load from OpenHands/extensions, while local skills live in `local_skills/` and are merged separately. -Additionally, OpenHands extends the schema with an optional `skills[]` array for listing skills directly (these are treated as direct skill sources, not plugin bundles). +The guide below starts with the simplest direct loader calls (`load_skills_from_dir()` and `load_public_skills()`) so you can see exactly what each source contributes. The example repository still includes `.plugin/marketplace.json` because that is the configuration file used for repository-managed marketplace filtering. +Additionally, OpenHands extends the schema with an optional `skills[]` array for listing skills directly (these are treated as direct skill sources, not plugin bundles). ## Example: Combining Local and Remote Skills From 40b1924a66a83cfb200c3ec3c065e1d86fcd59d6 Mon Sep 17 00:00:00 2001 From: Engel Nyst Date: Sun, 8 Mar 2026 06:02:29 +0100 Subject: [PATCH 6/6] Apply suggestion from @enyst --- sdk/guides/mixed-marketplace-skills.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sdk/guides/mixed-marketplace-skills.mdx b/sdk/guides/mixed-marketplace-skills.mdx index c4fd54fe..fa4d6889 100644 --- a/sdk/guides/mixed-marketplace-skills.mdx +++ b/sdk/guides/mixed-marketplace-skills.mdx @@ -11,7 +11,7 @@ This guide shows how to combine locally-defined skills with remote skills from t Teams often need to: - Maintain custom workflow-specific skills locally -- Leverage community skills from OpenHands extensions +- Use skills from OpenHands extensions - Create curated skill sets for specific projects ## Loading Pattern