Plugin Management
This document covers the AgenC runtime plugin catalog and lifecycle management system. The catalog tracks all registered plugins, enforces slot exclusivity, res
Plugin Management
This document covers the AgenC runtime plugin catalog and lifecycle management system. The catalog tracks all registered plugins, enforces slot exclusivity, resolves precedence conflicts, and exposes CLI commands for day-to-day plugin administration.
Source: runtime/src/skills/catalog.ts
Table of Contents
- Installing a Custom LLM Plugin
Overview
The plugin catalog is the central registry that the AgenC runtime consults when loading, enabling, or disabling plugins. Every plugin installed into the runtime receives a CatalogEntry that records its manifest, precedence level, enabled state, slot assignment, and filesystem metadata.
The catalog is persisted to disk at .agenc/plugins.json and is updated atomically on every operation. At runtime startup, the catalog is loaded and each enabled plugin is initialized in slot-priority order.
Core Concepts
Catalog Entry
A CatalogEntry represents a single plugin registered in the catalog.
interface CatalogEntry {
/** The plugin's parsed manifest. */
manifest: PluginManifest;
/** Where this plugin was discovered: workspace, user, or builtin. */
precedence: PluginPrecedence;
/** Whether the plugin is currently active. */
enabled: boolean;
/** The logical slot this plugin occupies (e.g., "llm", "memory"). */
slot?: string;
/** Absolute path to the plugin source on disk. */
sourcePath?: string;
/** Unix timestamp (ms) when the plugin was first installed. */
installedAtMs: number;
/** Unix timestamp (ms) of the most recent modification. */
lastModifiedMs: number;
}Catalog State
The CatalogState is the top-level structure serialized to .agenc/plugins.json.
interface CatalogState {
/** Schema version for forward-compatible migrations. */
schemaVersion: number;
/** All registered plugins keyed by plugin ID (manifest.name). */
entries: Recordstring, CatalogEntry>;
/**
* Maps logical slot names to the plugin ID currently occupying
* that slot. Only one plugin per slot can be active at a time.
*/
slotAssignments: Recordstring, string>;
/** Unix timestamp (ms) of the last catalog modification. */
lastModifiedMs: number;
}Plugin Manifest
Every plugin ships a manifest that declares its identity, version, slot requirement, and dependencies on other plugins.
interface PluginManifest {
/** Unique plugin identifier (e.g., "deepseek-llm"). */
name: string;
/** Semantic version string. */
version: string;
/** The logical slot this plugin targets. */
slot: PluginSlotType;
/** Human-readable description. */
description?: string;
/** Other plugin names that must be installed and enabled first. */
dependencies?: string[];
}Manifests are validated during installation. A plugin with a malformed or incomplete manifest is rejected with a PluginCatalogError.
Plugin Slots
Slots define the functional category a plugin fills. The runtime defines five slot types:
type PluginSlotType =
| 'memory' // Persistent memory backends
| 'llm' // Language model providers
| 'proof' // ZK proof generation
| 'telemetry' // Metrics and observability
| 'custom'; // User-defined functionalityPlugin Precedence
Precedence determines which plugin wins when multiple plugins compete for the same slot. Lower numeric values indicate higher priority.
enum PluginPrecedence {
workspace = 0, // Project-local plugins (highest priority)
user = 1, // User-installed plugins
builtin = 2, // Bundled with the runtime (lowest priority)
}Resolution rule: when two plugins target the same slot, the plugin with the lower precedence value takes the slot automatically. If both share the same precedence level, the most recently installed plugin wins.
Lifecycle
State Diagram
The following diagram shows the states a plugin passes through from discovery to removal.
install
+-----------+ +-----------+
| Available |------>| Installed |
+-----------+ +-----+-----+
|
+------------+------------+
| (auto-enable if |
| slot is unoccupied) |
v v
+-----------+ +------------+
| Enabled |<---------->| Disabled |
+-----------+ enable/ +------------+
| disable |
| |
+------------+-----------+
|
| uninstall
v
+-------------+
| Uninstalled |
+-------------+A plugin may also be reloaded in-place while in the Enabled state. Reload transitions through Enabled -> (teardown) -> (re-init) -> Enabled without leaving the enabled state from the catalog's perspective.
State Transitions
| From | To | Trigger | Side Effects |
|---|---|---|---|
| Available | Installed | agenc plugin install | Manifest validated, entry created, slot assignment attempted |
| Installed | Enabled | Automatic on install (if slot free) or agenc plugin enable | Plugin initialize() called, slot assignment recorded |
| Enabled | Disabled | agenc plugin disable | Plugin teardown() called, slot freed |
| Disabled | Enabled | agenc plugin enable | Plugin initialize() called, slot re-acquired |
| Enabled | Enabled | agenc plugin reload | teardown() then initialize() called; catalog timestamps updated |
| Enabled / Disabled | Uninstalled | agenc plugin uninstall | teardown() called if enabled, entry removed, slot freed |
Slot Management
Slot Types
Each slot type corresponds to a runtime subsystem that accepts exactly one active provider at a time.
| Slot | Purpose | Example Plugins |
|---|---|---|
memory | Persistent memory backend (vector store, key-value) | sqlite-memory, pgvector-memory |
llm | Language model provider for inference | grok-llm, ollama-llm, deepseek-llm |
proof | ZK proof generation backend | noir-groth16, sp1-proof |
telemetry | Metrics collection and export | otel-telemetry, datadog-telemetry |
custom | User-defined slot name (no exclusivity) | Any user plugin |
Slot Exclusivity
Only one plugin can occupy a given logical slot at any time. When a plugin is installed into a slot that is already occupied, the catalog compares precedence levels:
- If the new plugin has higher precedence (lower numeric value), it replaces the incumbent automatically. The incumbent is disabled.
- If the new plugin has equal or lower precedence, the install succeeds but the plugin starts in the
Disabledstate. The incumbent retains the slot.
The custom slot type is the exception: it does not enforce exclusivity since custom plugins use arbitrary slot names that typically do not collide.
Collision Detection
When a slot collision is detected, the catalog produces a SlotCollision object describing the conflict.
interface SlotCollision {
/** The slot being contested. */
slot: string;
/** Plugin ID of the current slot occupant. */
incumbentId: string;
/** Precedence of the incumbent. */
incumbentPrecedence: PluginPrecedence;
/** Plugin ID of the incoming challenger. */
challengerId: string;
/** Precedence of the challenger. */
challengerPrecedence: PluginPrecedence;
/** True if the challenger will displace the incumbent. */
willDisplace: boolean;
}The catalog logs every collision as a warning-level event so operators can audit slot transitions.
Operations and Results
Every catalog mutation returns a CatalogOperationResult indicating success or failure and capturing the state change.
interface CatalogOperationResult {
/** Whether the operation completed without error. */
success: boolean;
/** The plugin ID the operation targeted. */
pluginId: string;
/** The operation that was performed. */
operation: 'install' | 'enable' | 'disable' | 'reload' | 'uninstall';
/** Human-readable summary of the outcome. */
message: string;
/** Catalog entry before the operation (undefined for install). */
previousState?: CatalogEntry;
/** Catalog entry after the operation (undefined for uninstall). */
newState?: CatalogEntry;
}Failed operations throw a PluginCatalogError (see Error Handling).
CLI Reference
All plugin commands are grouped under the agenc plugin subcommand.
agenc plugin list
Display all registered plugins and their current status.
agenc plugin listExample output:
Plugin Slot Status Precedence
--------------------- ---------- -------- ----------
grok-llm llm enabled builtin
sqlite-memory memory enabled builtin
noir-groth16 proof enabled builtin
otel-telemetry telemetry disabled user
deepseek-llm llm disabled userOptions:
| Flag | Description |
|---|---|
--enabled | Show only enabled plugins |
--slot <type> | Filter by slot type |
--json | Output as JSON |
agenc plugin install
Install a plugin by name or path and register it in the catalog.
agenc plugin install <name-or-path>The install process:
- Locate the plugin source (npm package, local path, or registry).
- Parse and validate the
PluginManifest. - Check declared dependencies against the catalog.
- Create a
CatalogEntryand attempt slot assignment. - If the slot is free, auto-enable the plugin.
# Install from the plugin registry
agenc plugin install deepseek-llm
# Install from a local directory
agenc plugin install ./my-plugins/custom-memoryagenc plugin enable
Re-enable a previously disabled plugin.
agenc plugin enable <name>Enabling a plugin assigns it to its declared slot. If the slot is occupied by another plugin of lower or equal precedence, the enable operation fails with a slot collision error. Use agenc plugin disable on the incumbent first, or install a plugin with higher precedence.
agenc plugin enable deepseek-llm
# Output: Enabled deepseek-llm (slot: llm). Displaced grok-llm (lower precedence).agenc plugin disable
Deactivate a plugin without removing it from the catalog.
agenc plugin disable <name>The plugin's teardown() method is called and its slot is freed. The catalog entry remains so the plugin can be re-enabled later without reinstalling.
agenc plugin disable otel-telemetry
# Output: Disabled otel-telemetry. Slot telemetry is now unoccupied.agenc plugin reload
Hot-reload a plugin that is currently enabled. The plugin is torn down and re-initialized with its latest source.
agenc plugin reload <name>This is useful during development when iterating on a plugin without restarting the entire runtime. The reload operation:
- Calls
teardown()on the running plugin instance. - Re-reads the plugin source from
sourcePath. - Re-validates the manifest.
- Calls
initialize()with the current runtime context.
agenc plugin reload deepseek-llm
# Output: Reloaded deepseek-llm. Downtime: 42ms.agenc plugin info
Display detailed information about a specific plugin, including its manifest, dependencies, slot assignment, and filesystem metadata.
agenc plugin info <name>Example output:
Plugin: deepseek-llm
Version: 1.2.0
Slot: llm
Status: enabled
Precedence: user
Source: /home/user/.agenc/plugins/deepseek-llm
Installed: 2026-02-10T14:30:00Z
Modified: 2026-02-14T09:15:00Z
Dependencies:
(none)
Manifest:
name: deepseek-llm
version: 1.2.0
slot: llm
description: DeepSeek model provider for AgenCagenc plugin uninstall
Remove a plugin from the catalog entirely.
agenc plugin uninstall <name>If the plugin is currently enabled, it is disabled first (calling teardown()). The catalog entry is deleted and the slot is freed.
agenc plugin uninstall otel-telemetry
# Output: Uninstalled otel-telemetry. Catalog entry removed.Catalog Storage
The catalog is persisted as a JSON file at the default path:
.agenc/plugins.jsonExample catalog file:
{
"schemaVersion": 1,
"entries": {
"grok-llm": {
"manifest": {
"name": "grok-llm",
"version": "1.0.0",
"slot": "llm",
"description": "xAI Grok model provider"
},
"precedence": 2,
"enabled": true,
"slot": "llm",
"sourcePath": "runtime/dist/plugins/grok-llm",
"installedAtMs": 1707580800000,
"lastModifiedMs": 1707580800000
},
"sqlite-memory": {
"manifest": {
"name": "sqlite-memory",
"version": "1.0.0",
"slot": "memory",
"description": "SQLite-backed memory provider"
},
"precedence": 2,
"enabled": true,
"slot": "memory",
"sourcePath": "runtime/dist/plugins/sqlite-memory",
"installedAtMs": 1707580800000,
"lastModifiedMs": 1707580800000
}
},
"slotAssignments": {
"llm": "grok-llm",
"memory": "sqlite-memory"
},
"lastModifiedMs": 1707580800000
}The catalog uses atomic writes (write to a temporary file, then rename) to prevent corruption during concurrent access.
Error Handling
Validation failures and illegal operations throw PluginCatalogError.
class PluginCatalogError extends Error {
constructor(
message: string,
public readonly code: PluginCatalogErrorCode,
public readonly pluginId?: string
) {
super(message);
this.name = 'PluginCatalogError';
}
}Common error codes:
| Code | Cause |
|---|---|
MANIFEST_INVALID | Manifest is missing required fields or has invalid values |
PLUGIN_NOT_FOUND | Referenced plugin ID does not exist in the catalog |
ALREADY_INSTALLED | A plugin with the same name is already registered |
SLOT_OCCUPIED | Target slot is held by a higher-precedence plugin |
DEPENDENCY_MISSING | One or more declared dependencies are not installed |
ALREADY_ENABLED | Plugin is already in the enabled state |
ALREADY_DISABLED | Plugin is already in the disabled state |
RELOAD_FAILED | Plugin re-initialization failed after teardown |
Examples
Installing a Custom LLM Plugin
Replace the built-in Grok LLM plugin with a user-installed DeepSeek provider.
# Install the plugin (precedence: user = 1, higher than builtin = 2)
agenc plugin install deepseek-llm
# The catalog detects a slot collision on "llm":
# incumbent: grok-llm (precedence: builtin/2)
# challenger: deepseek-llm (precedence: user/1)
# result: deepseek-llm displaces grok-llm automatically
# Verify the swap
agenc plugin list --slot llmExpected output:
Plugin Slot Status Precedence
--------------- ---- -------- ----------
deepseek-llm llm enabled user
grok-llm llm disabled builtinReplacing a Built-in Plugin
To revert back to the built-in plugin after disabling a user-installed one:
# Disable the user plugin
agenc plugin disable deepseek-llm
# The builtin plugin is not auto-promoted. Enable it explicitly.
agenc plugin enable grok-llm
# Confirm
agenc plugin info grok-llmInspecting Slot Assignments
View which plugins currently hold each slot:
agenc plugin list --json | jq '.slotAssignments'{
"llm": "deepseek-llm",
"memory": "sqlite-memory",
"proof": "noir-groth16"
}To find unoccupied slots, compare against the defined slot types:
agenc plugin list --json | jq '
["memory","llm","proof","telemetry"] - [.slotAssignments | keys[]]
'["telemetry"]