mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 01:05:27 -04:00
0187d14b83
`deepagents-code` now supports experimental plugin marketplaces with namespaced skills and MCP servers. Enable with `DEEPAGENTS_CODE_EXPERIMENTAL=1`. Docs: https://github.com/langchain-ai/docs/pull/4905 --- `deepagents-code` can now manage experimental plugin marketplaces and load installed plugins that contribute namespaced skills and MCP servers. The new plugin manager establishes the `tui/modals`, `tui/screens`, and `tui/widgets` UI convention, with colocated styling and focused state/content modules. - Supports local, GitHub, Git, and marketplace JSON sources. - Adds install, enable, disable, uninstall, and safe marketplace removal through `/plugins` and `dcode plugin`. - Reloads plugin skills and plugin MCP configuration with `/reload` when experimental mode is enabled. - Namespaces plugin skills as `{plugin_id}:{skill_name}` through a code-local `PluginSkillsMiddleware` adapter, so no SDK changes are required. Made by [Open SWE](https://openswe.vercel.app/agents/019f5dea-0f1f-73fe-9803-32d1b13fbd4c) --------- Signed-off-by: Johannes du Plessis <johannes@langchain.dev> Co-authored-by: Alexander Olsen <aolsenjazz@gmail.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Mason Daugherty <mason@langchain.dev> Co-authored-by: open-swe[bot] <215916821+open-swe[bot]@users.noreply.github.com> Co-authored-by: Mason Daugherty <github@mdrxy.com>
122 lines
3.7 KiB
Python
122 lines
3.7 KiB
Python
"""Adapter from discovered plugins to `SkillsMiddleware` sources."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, TypeAlias
|
|
|
|
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
|
|
|
|
if TYPE_CHECKING:
|
|
from deepagents_code.plugins.models import PluginInstance
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SkillPath: TypeAlias = str
|
|
SkillLabel: TypeAlias = str
|
|
SkillNamespace: TypeAlias = str
|
|
DirectorySkillSource: TypeAlias = tuple[SkillPath, SkillLabel]
|
|
PluginSkillSource: TypeAlias = tuple[SkillPath, SkillLabel, SkillNamespace]
|
|
CodeSkillSource: TypeAlias = DirectorySkillSource | PluginSkillSource
|
|
|
|
|
|
def namespaced_skill_name(
|
|
namespace: SkillNamespace,
|
|
name: str,
|
|
subfolders: tuple[str, ...] = (),
|
|
) -> str:
|
|
"""Qualify a skill name under its plugin namespace.
|
|
|
|
Nested skill directories contribute intermediate `:`-joined segments
|
|
between the plugin namespace and the skill name, matching the plugin skill
|
|
naming convention (e.g. `plugin:sub:review`).
|
|
|
|
Args:
|
|
namespace: Plugin namespace (its `plugin_id`).
|
|
name: Skill name from the skill's frontmatter.
|
|
subfolders: Directory names between the plugin skills root and the
|
|
skill directory, in path order.
|
|
|
|
Returns:
|
|
The qualified skill name.
|
|
"""
|
|
return ":".join((namespace, *subfolders, name)).lower()
|
|
|
|
|
|
def plugin_skill_sources(
|
|
plugins: tuple[PluginInstance, ...],
|
|
) -> list[PluginSkillSource]:
|
|
"""Return skill source tuples for plugin skills.
|
|
|
|
Args:
|
|
plugins: Plugin instances.
|
|
|
|
Returns:
|
|
Source tuples containing path, label, and plugin namespace.
|
|
"""
|
|
sources: list[PluginSkillSource] = []
|
|
for plugin in plugins:
|
|
for path in plugin.inventory.skills:
|
|
source_path = path.parent if path.name == "SKILL.md" else path
|
|
try:
|
|
if not source_path.exists():
|
|
continue
|
|
except OSError:
|
|
logger.warning("Could not inspect plugin skill path %s", source_path)
|
|
continue
|
|
sources.append(
|
|
(
|
|
str(source_path),
|
|
f"Plugin: {plugin.plugin_id}",
|
|
plugin.plugin_id,
|
|
)
|
|
)
|
|
return sources
|
|
|
|
|
|
def plugin_skill_roots(plugins: tuple[PluginInstance, ...]) -> list[Path]:
|
|
"""Return plugin skill roots for skill-content containment checks.
|
|
|
|
Args:
|
|
plugins: Discovered plugin instances.
|
|
|
|
Returns:
|
|
Skill root directories.
|
|
"""
|
|
roots: list[Path] = []
|
|
for plugin in plugins:
|
|
roots.extend(
|
|
path.parent if path.name == "SKILL.md" else path
|
|
for path in plugin.inventory.skills
|
|
)
|
|
return roots
|
|
|
|
|
|
def discover_plugin_skill_sources_and_roots() -> tuple[
|
|
tuple[tuple[Path, str], ...], tuple[Path, ...]
|
|
]:
|
|
"""Discover plugin skill sources and containment roots.
|
|
|
|
Returns:
|
|
Plugin skill sources and roots, or empty tuples when plugins are disabled
|
|
or discovery fails.
|
|
"""
|
|
plugin_sources: tuple[tuple[Path, str], ...] = ()
|
|
plugin_roots: tuple[Path, ...] = ()
|
|
try:
|
|
if is_env_truthy(EXPERIMENTAL):
|
|
from deepagents_code.plugins import discover_plugins
|
|
|
|
plugins = discover_plugins().plugins
|
|
plugin_sources = tuple(
|
|
(Path(path), namespace)
|
|
for path, _label, namespace in plugin_skill_sources(plugins)
|
|
)
|
|
plugin_roots = tuple(plugin_skill_roots(plugins))
|
|
except (OSError, RuntimeError):
|
|
logger.warning("Could not discover plugin skills", exc_info=True)
|
|
return (), ()
|
|
|
|
return plugin_sources, plugin_roots
|