Plugin Development
RivetOS supports five plugin types: Providers (talk to LLMs), Channels (send/receive messages), Tools (agent capabilities), Memory (persistent storage), and Transports (expose RivetOS to external clients). Every plugin uses the same self-registration contract.
Quick Start: Scaffold a Plugin
Section titled “Quick Start: Scaffold a Plugin”npx rivetos plugin init# or directly:npx nx g @rivetos/nx:plugin# ? What type of plugin? › channel# ? What is the plugin name? › slackThis creates plugins/{category}/{name}/ with package.json, tsconfig.json, src/index.ts, and a test file.
Architecture Rules
Section titled “Architecture Rules”- Depend on
@rivetos/typesonly. Plugins do not import from@rivetos/coreor@rivetos/boot. (The memory plugin’s workers and the MCP server transport are exceptions because they ship binaries that run outside the runtime.) - Export a
manifest: PluginManifestconst. This is the entry point boot uses. - Declare
package.json#rivetos. This is what discovery reads — without importing the package. - Handle platform concerns internally. Message splitting, rate limits, API quirks — all inside the plugin.
package.json#rivetos
Section titled “package.json#rivetos”{ "name": "@rivetos/channel-slack", "rivetos": { "type": "channel", "name": "slack" }}Boot scans every plugins/*/*/package.json (and any plugin_dirs from config). The descriptor’s type and name must match the exported manifest.
The Manifest Contract
Section titled “The Manifest Contract”import type { PluginManifest, RegistrationContext } from '@rivetos/types'
export const manifest: PluginManifest = { type: 'provider', // 'provider' | 'channel' | 'tool' | 'memory' | 'transport' name: 'mistral', async register(ctx: RegistrationContext) { // 1. Read your config slice const cfg = ctx.pluginConfig as { model?: string; api_key?: string } | undefined const apiKey = cfg?.api_key ?? ctx.env.MISTRAL_API_KEY ?? ''
// 2. Construct your plugin const provider = new MistralProvider({ model: cfg?.model ?? 'mistral-large-latest', apiKey })
// 3. Register with the runtime ctx.registerProvider(provider)
// 4. (Optional) Register cleanup ctx.registerShutdown(() => provider.close()) },}RegistrationContext exposes:
| Member | Purpose |
|---|---|
config | Full validated RivetConfig (cast in your plugin if you need it) |
pluginConfig | The slice for this plugin — config.providers[name], config.channels[name], config.memory[name], config.transports[name]. undefined for tool plugins (tools read from config directly when needed). |
env | process.env snapshot |
workspaceDir | Resolved workspace path |
logger | Scoped logger (debug / info / warn / error) |
registerProvider / registerChannel / registerTool / registerMemory | Hand instances to the runtime |
registerHook(hook) | Subscribe to lifecycle events |
registerShutdown(fn) | Called during graceful shutdown |
lateBindTool(name) | Returns a closure that resolves a tool at execution time — used by composite tools when registration order isn’t guaranteed |
onRegistrationComplete(fn) | Fires once after every plugin has registered. Receives { tools }. Used by transports to enumerate the finalized tool set before opening their listening socket. |
Boot has no per-plugin knowledge. Every kind of plugin goes through the same loader (packages/boot/src/registrars/plugins.ts).
Activation
Section titled “Activation”A plugin is discovered by package.json#rivetos, but only activated when:
- Provider / channel / memory / transport — its name appears in the matching config section (
config.providers[name],config.channels[name],config.memory[name],config.transports[name]). Channels also accept a legacy alias:voice-discordmatchesconfig.channels.voice. - Tool — always activated (tools decide internally whether their config is sufficient — e.g.
mcp-clientskips itself when no servers are configured).
Provider Plugin
Section titled “Provider Plugin”interface Provider { id: string name: string chatStream(messages: Message[], options?: ChatOptions): AsyncIterable<LLMChunk> chat?(messages: Message[], options?: ChatOptions): Promise<LLMResponse> isAvailable(): Promise<boolean> getModel(): string setModel(model: string): void}The AgentLoop always calls chatStream(). chat() is optional. getModel() / setModel() enable runtime model switching via /model.
LLMChunk
Section titled “LLMChunk”interface LLMChunk { type: 'text' | 'tool_call' | 'thinking' | 'done' | 'error' text?: string toolCall?: ToolCall usage?: TokenUsage error?: string}Reference implementations
Section titled “Reference implementations”| Provider | Path | Notable |
|---|---|---|
| Anthropic | plugins/providers/anthropic/ | Adaptive thinking, prompt caching |
| xAI | plugins/providers/xai/ | Live search, conversation caching |
plugins/providers/google/ | Thought signatures for function calling | |
| Ollama | plugins/providers/ollama/ | Native API |
| vllm | plugins/providers/vllm/ | vLLM server — full vLLM surface (sampling extensions, mm/chat_template kwargs, video, reasoning_content); folds mid-conversation system messages |
| llama-server | plugins/providers/llama-server/ | llama.cpp llama-server — lean (top_k/min_p + extra_body); folds mid-conversation system messages |
| claude-cli | plugins/providers/claude-cli/ | Drives the claude binary via stream-json; embedded MCP bridge for hybrid tools |
Channel Plugin
Section titled “Channel Plugin”interface Channel { id: string platform: string start(): Promise<void> stop(): Promise<void> send(message: OutboundMessage): Promise<string | null> edit?(channelId, messageId, text, overflowIds?): Promise<EditResult | null> react?(messageId, emoji, channelId): Promise<void> startTyping?(channelId): void stopTyping?(channelId): void resolveAttachment?(attachment): Promise<ResolvedAttachment | null> onMessage(handler: MessageHandler): void onCommand(handler: CommandHandler): void}The runtime calls edit() repeatedly while streaming. Channels handle:
- Throttling — don’t hit the platform on every token
- Splitting — when text exceeds the platform limit, split into overflow messages and report the IDs back via
EditResult - Typing — show while the agent is working
Reference implementations
Section titled “Reference implementations”| Channel | Path | Notable |
|---|---|---|
| Discord | plugins/channels/discord/ | Streaming edits, overflow, reactions, embeds |
| Telegram | plugins/channels/telegram/ | Owner gate, inline keyboards, 4096-char splitting |
| Agent | plugins/channels/agent/ | HTTPS/mTLS inter-agent + mesh endpoints |
| Voice (Discord) | plugins/channels/voice-discord/ | xAI Realtime API + Opus codec |
Tool Plugin
Section titled “Tool Plugin”interface Tool extends ToolDefinition { execute(args, signal?, context?): Promise<ToolResult>}
interface ToolDefinition { name: string description: string parameters: object // JSON Schema}signal—AbortSignalfrom the turn. Honor it.context— workspace path, agent name, config, etc.ToolResult—stringfor text,ContentPart[]for multimodal (text + images).
Reference implementations
Section titled “Reference implementations”| Tool plugin | Tools registered | Notable |
|---|---|---|
tool-shell | shell | Safety categorization, cwd tracking, timeout |
tool-file | file_read, file_write, file_edit | Surgical edits, line numbers, optional backups |
tool-search | search_glob, search_grep | Glob and grep with file pattern filtering |
tool-web-search | internet_search, web_fetch | Google CSE + DuckDuckGo fallback, HTML → markdown |
tool-interaction | ask_user, todo | Structured questions, session-scoped task list |
tool-mcp-client | dynamic | Connects to MCP servers (stdio + HTTP), exposes their tools |
The memory plugin (@rivetos/memory-postgres) additionally registers memory_search, memory_browse, memory_stats. Delegation, sub-agents, and skill management add delegate_task, subagent_*, and skill_* tools at runtime.
Memory Plugin
Section titled “Memory Plugin”interface Memory { append(entry: MemoryEntry): Promise<string> search(query, options?): Promise<MemorySearchResult[]> getContextForTurn(query, agent, options?): Promise<string> getSessionHistory(sessionId, options?): Promise<Message[]> saveSessionSettings?(sessionId, settings): Promise<void> loadSessionSettings?(sessionId): Promise<Record<string, unknown> | null>}The PostgreSQL memory plugin (plugins/memory/postgres/) is the reference. It implements full transcript storage, hybrid FTS + vector search, summary DAG (hierarchical compaction), event-driven embedding/compaction workers (running as Datahub services on Postgres LISTEN/NOTIFY), temporal decay scoring, and a review loop for pattern extraction. SQL DDL lives co-located in plugins/memory/postgres/schema/.
See MEMORY-DESIGN.md for the full design.
Transport Plugin
Section titled “Transport Plugin”Transports expose RivetOS to external clients. They have no core interface — the plugin opens its own listening surface (HTTP, stdio, gRPC, …) inside manifest.register().
export const manifest: PluginManifest = { type: 'transport', name: 'mcp', async register(ctx) { const cfg = ctx.pluginConfig ?? {} const server = createMcpServer(cfg)
// Defer binding until every other plugin has registered, so the // transport sees the full tool set. ctx.onRegistrationComplete(async ({ tools }) => { server.registerTools(tools) await server.listen() })
ctx.registerShutdown(() => server.close()) },}Activate via config.transports.<name>:
transports: mcp: port: 4321 tls: trueReference: plugins/transports/mcp-server/ — a StreamableHTTP MCP server exposing memory_*, web_*, skill_*, and runtime tools to external MCP clients.
Testing
Section titled “Testing”npx nx run provider-mistral:test # one pluginnpx nx affected -t test # only what changedCo-locate tests next to source: src/index.ts → src/index.test.ts. The framework is Vitest.
Package Structure
Section titled “Package Structure”plugins/{category}/{name}/├── package.json # @rivetos/{category}-{name} (or @rivetos/{name} for transports)├── tsconfig.json # extends ../../../tsconfig.base.json├── eslint.config.mjs # inherits shared config└── src/ ├── index.ts # exports `manifest` + your impl ├── index.test.ts └── ...package.json must include the rivetos descriptor:
{ "name": "@rivetos/provider-mistral", "version": "0.4.0-beta.x", "private": true, "rivetos": { "type": "provider", "name": "mistral" }, "dependencies": { "@rivetos/types": "workspace:*" }}