mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 09:15:22 -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>
228 lines
6.0 KiB
Python
228 lines
6.0 KiB
Python
"""Data models for plugins."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Literal
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
MarketplaceSourceType = Literal["directory", "file", "github", "git", "url"]
|
|
ExternalPluginRepositorySourceType = Literal["github", "git-subdir", "url"]
|
|
JsonValue = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
|
|
JsonObject = dict[str, JsonValue]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class LocalMarketplaceSource:
|
|
"""Local directory or JSON file used as a marketplace source."""
|
|
|
|
source_type: Literal["directory", "file"]
|
|
value: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class RepositoryMarketplaceSource:
|
|
"""GitHub or Git repository used as a marketplace source.
|
|
|
|
`ref` selects an optional branch or tag. Commit SHA checkout is not part of
|
|
the shallow-clone flow.
|
|
"""
|
|
|
|
source_type: Literal["github", "git"]
|
|
value: str
|
|
ref: str | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class UrlMarketplaceSource:
|
|
"""Marketplace manifest downloaded from an HTTP URL."""
|
|
|
|
source_type: Literal["url"]
|
|
value: str
|
|
|
|
|
|
MarketplaceSource = (
|
|
LocalMarketplaceSource | RepositoryMarketplaceSource | UrlMarketplaceSource
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class PluginManifest:
|
|
"""Parsed plugin manifest.
|
|
|
|
Attributes:
|
|
name: Plugin name from the manifest, or `None` for manifest-less plugins.
|
|
version: Version string from the plugin manifest.
|
|
component_paths: Validated skill and MCP paths keyed by component name.
|
|
inline_mcp: Inline MCP servers declared in the manifest.
|
|
"""
|
|
|
|
name: str | None
|
|
version: str | None
|
|
component_paths: dict[str, tuple[Path, ...]]
|
|
inline_mcp: JsonObject
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class ComponentInventory:
|
|
"""Inventory of supported plugin components."""
|
|
|
|
skills: tuple[Path, ...] = ()
|
|
mcp_files: tuple[Path, ...] = ()
|
|
warnings: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class PluginInstance:
|
|
"""A discovered plugin ready to feed dcode adapters.
|
|
|
|
Attributes:
|
|
plugin_id: Stable id in `{name}@{marketplace}` form.
|
|
name: Plugin namespace name.
|
|
marketplace: Parent marketplace used for identity and namespacing.
|
|
version: Version declared by the plugin manifest, if any.
|
|
root: Plugin root directory.
|
|
data_dir: Writable data directory for this plugin.
|
|
manifest: Parsed manifest, if any.
|
|
inventory: Component inventory.
|
|
"""
|
|
|
|
plugin_id: str
|
|
name: str
|
|
marketplace: str
|
|
version: str | None
|
|
root: Path
|
|
data_dir: Path
|
|
manifest: PluginManifest | None
|
|
inventory: ComponentInventory
|
|
|
|
def __post_init__(self) -> None:
|
|
"""Validate the canonical plugin identity.
|
|
|
|
Raises:
|
|
ValueError: If `plugin_id` disagrees with `name` and `marketplace`.
|
|
"""
|
|
expected = f"{self.name}@{self.marketplace}"
|
|
if self.plugin_id != expected:
|
|
msg = f"Plugin id {self.plugin_id!r} does not match {expected!r}"
|
|
raise ValueError(msg)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class LocalPluginSource:
|
|
"""A plugin stored relative to its marketplace."""
|
|
|
|
source_type: Literal["local"]
|
|
path: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class GithubPluginSource:
|
|
"""A plugin sourced from a GitHub repository."""
|
|
|
|
source_type: Literal["github"]
|
|
repo: str
|
|
ref: str | None = None
|
|
path: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class GitSubdirectoryPluginSource:
|
|
"""A plugin sourced from a subdirectory in a Git repository."""
|
|
|
|
source_type: Literal["git-subdir"]
|
|
url: str
|
|
ref: str | None = None
|
|
path: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class UrlPluginSource:
|
|
"""A plugin sourced from a Git repository URL."""
|
|
|
|
source_type: Literal["url"]
|
|
url: str
|
|
ref: str | None = None
|
|
path: str | None = None
|
|
|
|
|
|
PluginSource = (
|
|
LocalPluginSource
|
|
| GithubPluginSource
|
|
| GitSubdirectoryPluginSource
|
|
| UrlPluginSource
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class MarketplacePluginEntry:
|
|
"""A catalog entry from a marketplace manifest."""
|
|
|
|
name: str
|
|
source: PluginSource
|
|
description: str | None = None
|
|
author: str | JsonObject | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class PluginMarketplace:
|
|
"""A parsed marketplace manifest."""
|
|
|
|
name: str
|
|
root: Path
|
|
manifest_path: Path
|
|
metadata: JsonObject
|
|
plugins: tuple[MarketplacePluginEntry, ...]
|
|
warnings: tuple[str, ...] = ()
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class MarketplaceRecord:
|
|
"""Persisted marketplace source record."""
|
|
|
|
name: str
|
|
source_type: MarketplaceSourceType
|
|
source: str
|
|
install_location: str
|
|
ref: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class InstalledPluginEntry:
|
|
"""Install record for a plugin.
|
|
|
|
`version` is the value declared by the plugin manifest, if any.
|
|
"""
|
|
|
|
install_path: str
|
|
version: str | None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True, kw_only=True)
|
|
class PluginDiscoveryResult:
|
|
"""Result from plugin discovery."""
|
|
|
|
plugins: tuple[PluginInstance, ...]
|
|
warnings: tuple[str, ...] = ()
|
|
|
|
|
|
def split_plugin_id(plugin_id: str) -> tuple[str, str]:
|
|
"""Split a plugin id in `{plugin}@{marketplace}` form.
|
|
|
|
Returns:
|
|
Plugin and marketplace names.
|
|
|
|
Raises:
|
|
ValueError: If either part is missing.
|
|
"""
|
|
if "@" not in plugin_id:
|
|
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
|
|
raise ValueError(msg)
|
|
plugin, marketplace = plugin_id.rsplit("@", 1)
|
|
if not plugin or not marketplace:
|
|
msg = f"Invalid plugin id {plugin_id!r}; expected name@marketplace"
|
|
raise ValueError(msg)
|
|
return plugin, marketplace
|