Plugins¶
Plugins extend Replio with tools, providers, slash commands, and services without changing the core. The core stays stdlib-only. Any third-party dependencies live inside the plugin and are imported lazily, so they only matter when you install and use that plugin.
Installation locations¶
| Root | Scope | Precedence |
|---|---|---|
replio.plugins.bundled |
bundled with replio (shipped in the package) | lowest |
~/.config/replio/plugins/ |
global, all projects | middle |
.replio/plugins/ |
local to a project | highest (wins on name collision) |
First-party plugins ship with replio and are listed in the default plugins config, so they are active out of the box. replio-core-web provides web_search and web_fetch. replio-core-fs provides file_read, list_dir, file_write, glob, and grep. replio-core-exec provides run_command. replio-core-mcp provides the MCP client (mcp_connect/mcp_list/mcp_disconnect) and server (replio mcp and POST /mcp) - see mcp.md. replio-core-eval provides the eval fixture catalog for replio eval - see eval.md. They behave like any other plugin. Remove a name from plugins (or use /plugins disable) to stop it loading, but they cannot be uninstalled or updated, since they version with replio. A global or local plugin with the same name overrides the bundled one.
Plugin layout¶
A plugin is a directory with a manifest.json, an entry module, and an optional unit-test suite. Source modules live under src/ and tests under tests/ (a bare .py file at the root is also accepted - name = filename, defaults apply):
~/.config/replio/plugins/web-scraper/
manifest.json
src/
plugin.py # entry module (manifest "entry" points here)
helpers.py # sibling modules, importable by the entry
tests/
test_plugins.py # optional - run by `replio plugins test` and the core suite
The entry module may sit anywhere under the plugin directory - manifest.json "entry" is a path relative to the plugin root (default plugin.py). Sibling imports resolve from the entry module's own directory, so a src/ layout works the same as a flat one.
Manifest¶
{
"name": "replio-web-scraper",
"version": "0.3.1",
"description": "Full-page scraping with links and structure",
"replio_version": ">=0.12.0,<1.0",
"python": ">=3.10",
"entry": "plugin.py",
"requires": ["beautifulsoup4", "lxml"],
"provides": {"tools": ["scrape_page"], "providers": [], "commands": ["/scrape"]},
"source": "https://github.com/example/replio-web-scraper"
}
| Key | Default | Description |
|---|---|---|
name |
(required) | Plugin name, also the install directory name |
version |
"0.0.0" |
Plugin version |
description |
"" |
Shown in /plugins and replio plugins list |
replio_version |
"" |
Semver range the plugin is compatible with (>=0.12.0,<1.0). Incompatible plugins are skipped at load |
python |
"" |
Minimum/maximum Python, same range syntax (>=3.10) |
entry |
"plugin.py" |
Module to load, relative to the plugin directory (may point into src/) |
requires |
[] |
Third-party packages, metadata for status and --deps install, never imported by the core |
provides |
{} |
Declared tools/providers/commands for /plugins display |
source |
"" |
Origin recorded on install, used by update |
Entry contract¶
The entry module may define any of eight hooks (all optional):
def register_tools(registry) -> None: ... # @registry.register(...) - same as core tools
def register_providers(providers) -> None: ... # providers["name"] = ProviderClass
def register_commands(commands) -> None: ... # @commands.register(...) - same as core commands
def register_services(services) -> None: ... # services["name"] = service object for core features
def register_types(registry) -> None: ... # registry.add_plugin({...}) - plugin-owned types
def register_teams(teams) -> None: ... # register into the TeamRegistry (see swarm.md)
def register_skills(skills) -> None: ... # skills.add_plugin({...}) - see skills.md
def register_fixtures(fixtures) -> None: ... # fixtures["id"] = fixture data - see eval.md
Plugin tools automatically inherit the tool permission policy, /tool, /help, query refinement, noise_tools, and session logging. The loop never special-cases plugin names.
A tool handler may declare a _config keyword argument to receive the engine's Config (e.g. to read a config key like tool_max_result_chars). The registry passes it only when the handler's signature accepts it, and it is never exposed to the model. See tools.md.
Providers¶
register_providers contributes to the same provider set as the core PROVIDERS dict: the plugin provider appears in the /connect picker, and passing its DEFAULT_BASE_URL as a /connect <url> argument selects it automatically (see providers.md). A plugin provider's DEFAULT_BASE_URL also makes it a model-ref target (<name>/<model>).
Services¶
register_services lets a plugin power a core feature that is not tool-calling. Today the only service is the web search-then-answer mode (web_search: true). The bundled replio-core-web registers services['search'] with search(query, num), display(query, results), and context(query, results) methods. If no plugin registers the service, that mode reports that it is unavailable instead of erroring.
Agent types, teams, and skills¶
register_types(registry) contributes types to the TypeRegistry via registry.add_plugin(entry) (same entry shape as types.json). Plugin types form an in-memory layer between bundled and global, so precedence is bundled < plugin < global < local, and a types.json entry can always override or replace a plugin-provided type. register_teams(teams) and register_skills(skills) register into the team and skills registries the same way (teams.add_plugin(...) / skills.add_plugin(...), entry shapes in teams.md and skills.md). The /type list marks plugin types (plugin), and after /plugins install/update/uninstall the running REPL re-applies all three hooks immediately. Tools and commands still activate on the next start.
Eval fixtures¶
register_fixtures(fixtures) contributes task fixtures to the tool-use evaluation harness. The hook receives a dict of fixture id to fixture data (same shape as the JSON fixtures under .replio/eval/, see eval.md). Local and global fixture files override plugin fixtures by id. The bundled replio-core-eval plugin ships the default catalog this way.
Lazy dependencies¶
Keep third-party imports inside the tool function, not at module top level. A missing dependency then surfaces as a normal tool result with install guidance:
def register_tools(registry):
@registry.register(name='pdf2text', description='Extract text from a PDF',
parameters={'type': 'object', 'properties': {'path': {'type': 'string'}},
'required': ['path']})
def pdf2text(path):
try:
from pypdf import PdfReader
except ImportError:
return 'Error: pdf2text requires "pypdf" - pip install pypdf'
...
This is why the core stays zero-dependency. Plugin packages are only imported in the process that runs your configured plugins, and only when their tools are actually called.
Managing plugins¶
Config activation¶
{
"plugins": ["replio-core-web", "replio-core-fs", "replio-core-exec"]
}
pluginsis the list of plugins to load. Empty ([]) = all discovered plugins load.- The default config lists the bundled plugins so they are active by default. Remove a name (or
/plugins disable) to stop that plugin loading. /plugins enable <name>appends a name./plugins installand/plugins uninstalladd or remove the name automatically.- Changes apply on the next start (plugins load once at engine init).
plugins.enabled/plugins.denyfrom earlier versions are migrated automatically.
REPL¶
/plugins # list plugins
/plugins <name> # detail: manifest, deps, status
/plugins enable <name> # add to the plugins list, applies next start
/plugins disable <name> # remove from the plugins list, applies next start
/plugins install <git-url|path> [--global] [--deps]
/plugins update <name> # re-fetch from the recorded source
/plugins uninstall <name>
CLI¶
The same operations are available headless (for example before a CI replio run):
replio plugins list
replio plugins install <git-url|path> --deps
replio plugins update <name>
replio plugins uninstall <name>
replio plugins test [name]
installclones a git URL or copies a local directory into.replio/plugins/(or~/.config/replio/plugins/with--global), recordssource, and with--depsrunspip installon the declaredrequires.updaterunsgit pullfor remote sources or re-copies a local path.testruns a plugin'stests/unit suite (--verbosefor per-test output). Without a name it runs every plugin that has one. The core test suite also runs these throughtests/test_plugin_suites.py.- Bundled plugins report an error for
updateanduninstall. Disable them instead.
Status¶
/plugins (and replio plugins list) shows each plugin's name, version, origin (bundled / global / local), load status, and any unmet requires:
loaded- activedisabled- not in thepluginslist (when it is non-empty)incompatible-replio_versionorpythonrange not satisfied (reason shown)error- invalid manifest, missing entry module, or the entry module raised while loading
Security¶
Plugins are arbitrary Python code that run with your user's privileges. Install only plugins you trust. A plugin's register_providers hook runs at load. Its tools run on demand like any built-in tool.
Future paths¶
- Dependency isolation: today plugin deps install into the same Python environment (lazy imports keep the core clean). Shared-plugin and per-plugin virtualenvs are planned for stronger separation.
- PyPI source: the same hooks will be discoverable through
importlib.metadataentry points, so plugins can be distributed as regular packages. - Externalizing bundled plugins: the bundled
replio-core-*plugins are the migration path for optional features. Web and machine tools now ship through them, and they can be forked or superseded by global/local plugins of the same name.