Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 203 additions & 53 deletions sdk/guides/plugins.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -196,93 +196,243 @@ Brief explanation on how to use a plugin with an agent.

## Ready-to-run Example

The example below demonstrates plugin loading via Conversation and plugin management utilities (install, list, update, uninstall).

<Note>
This example is available on GitHub: [examples/05_skills_and_plugins/02_loading_plugins/main.py](https://github.com/OpenHands/software-agent-sdk/blob/main/examples/05_skills_and_plugins/02_loading_plugins/main.py)
</Note>

```python icon="python" expandable examples/05_skills_and_plugins/02_loading_plugins/main.py
"""Example: Loading Plugins via Conversation
"""Example: Loading and Managing Plugins

This example demonstrates plugin loading and management in the SDK:

1. Loading plugins via Conversation (PluginSource)
2. Installing plugins to persistent storage
3. Listing, updating, and uninstalling plugins

Demonstrates the recommended way to load plugins using the `plugins` parameter
on Conversation. Plugins bundle skills, hooks, and MCP config together.
Plugins bundle skills, hooks, and MCP config together.

Supported plugin sources:
- Local path: /path/to/plugin
- GitHub shorthand: github:owner/repo
- Git URL: https://github.com/owner/repo.git
- With ref: branch, tag, or commit SHA
- With repo_path: subdirectory for monorepos

For full documentation, see: https://docs.all-hands.dev/sdk/guides/plugins
"""

import os
import sys
import tempfile
from pathlib import Path

from pydantic import SecretStr

from openhands.sdk import LLM, Agent, Conversation
from openhands.sdk.plugin import PluginSource
from openhands.sdk.plugin import (
PluginFetchError,
PluginSource,
install_plugin,
list_installed_plugins,
load_installed_plugins,
uninstall_plugin,
)
from openhands.sdk.tool import Tool
from openhands.tools.file_editor import FileEditorTool
from openhands.tools.terminal import TerminalTool


# Locate example plugin directory
script_dir = Path(__file__).parent
plugin_path = script_dir / "example_plugins" / "code-quality"

# Define plugins to load
# Supported sources: local path, "github:owner/repo", or git URL
# Optional: ref (branch/tag/commit), repo_path (for monorepos)
plugins = [
PluginSource(source=str(plugin_path)),
# PluginSource(source="github:org/security-plugin", ref="v2.0.0"),
# PluginSource(source="github:org/monorepo", repo_path="plugins/logging"),
]

# Check for API key
api_key = os.getenv("LLM_API_KEY")
if not api_key:
print("Set LLM_API_KEY to run this example")
print("EXAMPLE_COST: 0")
sys.exit(0)

# Configure LLM and Agent
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
llm = LLM(
usage_id="plugin-demo",
model=model,
api_key=SecretStr(api_key),
base_url=os.getenv("LLM_BASE_URL"),
)
agent = Agent(
llm=llm, tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)]
)
local_plugin_path = script_dir / "example_plugins" / "code-quality"

# Create conversation with plugins - skills, MCP config, and hooks are merged
# Note: Plugins are loaded lazily on first send_message() or run() call
with tempfile.TemporaryDirectory() as tmpdir:
conversation = Conversation(
agent=agent,
workspace=tmpdir,
plugins=plugins,
)

# Test: The "lint" keyword triggers the python-linting skill
# This first send_message() call triggers lazy plugin loading
conversation.send_message("How do I lint Python code? Brief answer please.")
def demo_conversation_with_plugins(llm: LLM) -> None:
"""Demo 1: Load plugins via Conversation's plugins parameter.

This is the recommended way to use plugins - they are loaded lazily
when the conversation starts.
"""
print("\n" + "=" * 60)
print("DEMO 1: Loading plugins via Conversation")
print("=" * 60)

# Define plugins to load
plugins = [
PluginSource(source=str(local_plugin_path)),
# Examples of other sources:
# PluginSource(source="github:owner/repo", ref="v1.0.0"),
# PluginSource(source="github:owner/monorepo", repo_path="plugins/my-plugin"),
]

# Verify skills were loaded from the plugin (after lazy loading)
skills = (
conversation.agent.agent_context.skills
if conversation.agent.agent_context
else []
agent = Agent(
llm=llm,
tools=[Tool(name=TerminalTool.name), Tool(name=FileEditorTool.name)],
)
print(f"Loaded {len(skills)} skill(s) from plugins")

conversation.run()
with tempfile.TemporaryDirectory() as tmpdir:
conversation = Conversation(
agent=agent,
workspace=tmpdir,
plugins=plugins,
)

# The "lint" keyword triggers the python-linting skill
conversation.send_message("How do I lint Python code? Brief answer please.")

# Verify skills were loaded
skills = (
conversation.agent.agent_context.skills
if conversation.agent.agent_context
else []
)
print(f"✓ Loaded {len(skills)} skill(s) from plugins")

conversation.run()


def demo_install_local_plugin(installed_dir: Path) -> None:
"""Demo 2: Install a plugin from a local path.

print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}")
Useful for development or local-only plugins.
"""
print("\n" + "=" * 60)
print("DEMO 2: Installing plugin from local path")
print("=" * 60)

info = install_plugin(source=str(local_plugin_path), installed_dir=installed_dir)
print(f"✓ Installed: {info.name} v{info.version}")
print(f" Source: {info.source}")
print(f" Path: {info.install_path}")


def demo_install_github_plugin(installed_dir: Path) -> None:
"""Demo 3: Install a plugin from GitHub.

Demonstrates the github:owner/repo shorthand with repo_path for monorepos.
"""
print("\n" + "=" * 60)
print("DEMO 3: Installing plugin from GitHub")
print("=" * 60)

try:
# Install from anthropics/skills repository
info = install_plugin(
source="github:anthropics/skills",
repo_path="skills/pptx",
ref="main",
installed_dir=installed_dir,
)
print(f"✓ Installed: {info.name} v{info.version}")
print(f" Source: {info.source}")
print(f" Resolved ref: {info.resolved_ref}")

except PluginFetchError as e:
print(f"⚠ Could not fetch from GitHub: {e}")
print(" (Network or rate limiting issue)")


def demo_list_and_load_plugins(installed_dir: Path) -> None:
"""Demo 4: List and load installed plugins."""
print("\n" + "=" * 60)
print("DEMO 4: List and load installed plugins")
print("=" * 60)

# List installed plugins
print("Installed plugins:")
for info in list_installed_plugins(installed_dir=installed_dir):
print(f" - {info.name} v{info.version} ({info.source})")

# Load plugins as Plugin objects
plugins = load_installed_plugins(installed_dir=installed_dir)
print(f"\nLoaded {len(plugins)} plugin(s):")
for plugin in plugins:
skills = plugin.get_all_skills()
print(f" - {plugin.name}: {len(skills)} skill(s)")


def demo_uninstall_plugins(installed_dir: Path) -> None:
"""Demo 5: Uninstall plugins."""
print("\n" + "=" * 60)
print("DEMO 5: Uninstalling plugins")
print("=" * 60)

for info in list_installed_plugins(installed_dir=installed_dir):
uninstall_plugin(info.name, installed_dir=installed_dir)
print(f"✓ Uninstalled: {info.name}")

remaining = list_installed_plugins(installed_dir=installed_dir)
print(f"\nRemaining plugins: {len(remaining)}")


# Main execution
if __name__ == "__main__":
api_key = os.getenv("LLM_API_KEY")
if not api_key:
print("Set LLM_API_KEY to run the full example")
print("Running install/uninstall demos only...")
llm = None
else:
model = os.getenv("LLM_MODEL", "anthropic/claude-sonnet-4-5-20250929")
llm = LLM(
usage_id="plugin-demo",
model=model,
api_key=SecretStr(api_key),
base_url=os.getenv("LLM_BASE_URL"),
)

with tempfile.TemporaryDirectory() as tmpdir:
installed_dir = Path(tmpdir) / "installed"
installed_dir.mkdir()

# Demo 1: Conversation with plugins (requires LLM)
if llm:
demo_conversation_with_plugins(llm)

# Demo 2-5: Plugin management (no LLM required)
demo_install_local_plugin(installed_dir)
demo_install_github_plugin(installed_dir)
demo_list_and_load_plugins(installed_dir)
demo_uninstall_plugins(installed_dir)

print("\n" + "=" * 60)
print("EXAMPLE COMPLETED SUCCESSFULLY")
print("=" * 60)

if llm:
print(f"EXAMPLE_COST: {llm.metrics.accumulated_cost:.4f}")
else:
print("EXAMPLE_COST: 0")
```

<RunExampleCode path_to_script="examples/05_skills_and_plugins/02_loading_plugins/main.py"/>

## Installing Plugins to Persistent Storage

The SDK provides utilities to install plugins to a local directory (`~/.openhands/plugins/installed/` by default). Installed plugins are tracked in `.installed.json`.

```python icon="python"
from openhands.sdk.plugin import (
install_plugin,
list_installed_plugins,
load_installed_plugins,
uninstall_plugin,
)

# Install from local path or GitHub
install_plugin(source="/path/to/plugin")
install_plugin(source="github:owner/repo", ref="v1.0.0")

# List and load installed plugins
for info in list_installed_plugins():
print(f"{info.name} v{info.version}")

plugins = load_installed_plugins()

# Uninstall
uninstall_plugin("plugin-name")
```

## Next Steps

Expand Down