--- title: Plugin System slug: guides/plugins public: true category: guides template: guide order: 4 description: Replace any Brainy subsystem — distance functions, embeddings, vector index, metadata index, aggregation — with a custom implementation or optional native acceleration. next: - guides/storage-adapters --- # Plugin Development Guide Brainy has a plugin system that allows third-party packages to replace internal subsystems with custom implementations. This is how `@soulcraft/cor` provides optional native acceleration, and it's the same system available to any developer. ## Architecture Overview Brainy's plugin system uses **named providers** — string keys mapped to implementations. During `init()`, brainy: 1. Imports each package listed in the `plugins` config array 2. Activates each plugin, passing a `BrainyPluginContext` 3. The plugin calls `context.registerProvider(key, implementation)` for each subsystem it provides 4. Brainy checks each provider key and wires the implementation into its internal pipeline Installing the first-party accelerator is the opt-in: with the default config, brainy probes for `@soulcraft/cor` and loads it when present. Everything except "not installed" fails **loud** — a present-but-broken accelerator makes `init()` throw rather than silently degrading to the JS engines. ```typescript const brain = new Brainy() // @soulcraft/cor auto-detected when installed const pinned = new Brainy({ plugins: ['@soulcraft/cor'] }) // or pin exactly what loads const plain = new Brainy({ plugins: [] }) // or opt out of detection entirely ``` | `plugins` value | Behavior | |---|---| | `undefined` (default) | Guarded auto-detection of `@soulcraft/cor`: not installed → no plugins, silently; installed → loads + announces; installed-but-broken → `init()` throws | | `false` / `[]` | No plugins, no detection (explicit opt-out) | | `['@soulcraft/cor']` | Load only the listed packages; a listed plugin that fails to load throws | Plugins registered programmatically via `brain.use(plugin)` are always activated regardless of the `plugins` config. If no plugin provides a given key, brainy uses its built-in JavaScript implementation. This means brainy works perfectly standalone — plugins only enhance performance or add capabilities. ## Creating a Plugin ### 1. Implement the `BrainyPlugin` interface ```typescript import type { BrainyPlugin, BrainyPluginContext } from '@soulcraft/brainy/plugin' const myPlugin: BrainyPlugin = { name: 'my-brainy-plugin', // Must be unique (typically your npm package name) async activate(context: BrainyPluginContext): Promise { // Register your providers here context.registerProvider('distance', myFastDistanceFunction) // Return true if activation succeeded, false to skip return true }, async deactivate(): Promise { // Optional cleanup when brainy.close() is called } } export default myPlugin ``` ### 2. Package exports Your package must export the plugin as the default export so brainy's plugin loader can resolve it: ```typescript // index.ts export { default } from './plugin.js' ``` ### 3. Registration **Config-based:** List your package name in the brainy config: ```typescript const brain = new Brainy({ plugins: ['my-brainy-plugin'] }) await brain.init() ``` **Programmatic registration:** For plugins not installed as npm packages, use `brain.use()`: ```typescript import { Brainy } from '@soulcraft/brainy' import myPlugin from './my-plugin.js' const brain = new Brainy() brain.use(myPlugin) await brain.init() ``` ## Provider Keys Reference Each key has a specific expected signature. Brainy checks for these during `init()` and wires them into the appropriate code paths. ### Core Providers #### `distance` **Type:** `(a: number[], b: number[]) => number` Replaces the default cosine distance function used in vector search and neural APIs. This is the highest-impact single provider — it's called for every vector comparison. ```typescript context.registerProvider('distance', (a: number[], b: number[]): number => { // Your SIMD-accelerated or GPU distance calculation return myFastCosineDistance(a, b) }) ``` #### `embeddings` **Type:** `(text: string | string[]) => Promise` Replaces the built-in WASM embedding engine. Called for every `brain.add()`, `brain.update()`, and `brain.find()` operation that involves text. ```typescript context.registerProvider('embeddings', async (text: string | string[]) => { if (Array.isArray(text)) { return myEngine.embedBatch(text) } return myEngine.embed(text) }) ``` #### `embedBatch` **Type:** `(texts: string[]) => Promise` Dedicated batch embedding provider. When registered, brainy uses this for bulk operations (import, reindex, batch add) instead of calling the `embeddings` provider N times. This enables true single-forward-pass batch processing. Priority order for batch operations: 1. `embedBatch` provider (single forward pass — fastest) 2. `embeddings` provider with `Promise.all()` (N individual calls) 3. Built-in WASM batch API (fallback) ```typescript context.registerProvider('embedBatch', async (texts: string[]) => { // Process all texts in a single forward pass return myEngine.batchEmbed(texts) }) ``` ### Index Providers > **Write-path invariant (the change-feed contract).** Every canonical > mutation flows through Brainy's generation-store commit points — index > providers are invoked *inside* that commit and never originate canonical > writes of their own. The `brain.onChange` change feed is emitted from those > commit points and relies on this: **a plugin must never introduce a write > path that bypasses the generation-store commit.** If a future provider ever > needs a direct native ingest path, it must either route through the commit > or emit equivalent change events — otherwise every `onChange` consumer > (live UIs, cache invalidation, realtime sync) silently develops a blind > spot. #### `vector` **Type:** `(config: object, distanceFunction: Function, options: object) => VectorIndexProvider-compatible` Factory function that creates a vector index instance. The returned object must implement the `VectorIndexProvider` public API: - `addItem(item: { id: string, vector: number[] }): Promise` - `search(queryVector: number[], k: number, filter?, options?): Promise>` - `removeItem(id: string): Promise` - `size(): number` - `clear(): void` - `flush(): Promise` - `rebuild(options?): Promise` - `getDirtyNodeCount(): number` - `getPersistMode(): 'immediate' | 'deferred'` - `getEntryPointId(): string | null` - `getMaxLevel(): number` - `getDimension(): number | null` - `getConfig(): object` - `getDistanceFunction(): Function` - `enableCOW(parent): void` - `setUseParallelization(boolean): void` For type-aware indexes (separate graph per noun type), also implement: - `getIndexForType(type: string): VectorIndexProvider` (duck-typed detection) - `search(queryVector, k, type?, filter?, options?): Promise>` ```typescript context.registerProvider('vector', (config, distanceFn, options) => { return new MyNativeVectorIndex(config, distanceFn, options) }) ``` #### The readiness contract (all three index providers) A provider that **persists its derived index** should implement the optional readiness members so a warm reopen never pays a redundant rebuild-from-canonical: - **`init?(): Promise`** — eager cold-load. Brainy awaits it once during `brain.init()`, after the metadata provider's `init()` (the id-mapper hydrates first) and **before the rebuild gate**. - **`isReady?(): boolean`** — honest durability signal. `true` ⇔ the persisted index is loaded (or cheaply demand-loadable) and consistent with what was last persisted. When exposed, the rebuild gate defers to this signal **instead of** the `size() === 0` / `totalEntries === 0` heuristics — a disk-native index may report 0 resident entries while fully durable. Never return `true` if the durable state failed to load: the signal is honest in both directions, and a not-ready provider gets its rebuild even when `size() > 0`. - **`isMigrating?(): boolean`** — while `true`, the provider owns its index (background migration); brainy skips its rebuild entirely. Providers that implement none of these keep the size/count heuristics — correct for engines whose `rebuild()` *is* their load path (like brainy's built-in JS vector index). #### `metadataIndex` **Type:** `(storage: StorageAdapter) => MetadataIndexManager-compatible` Factory function that creates a metadata index. The returned object must implement the `MetadataIndexManager` interface including `init()`, `addEntity()`, `removeEntity()`, `query()`, `flush()`, `clear()`, etc. ```typescript context.registerProvider('metadataIndex', (storage) => { return new MyNativeMetadataIndex(storage) }) ``` #### `graphIndex` **Type:** `(storage: StorageAdapter) => GraphAdjacencyIndex-compatible` Factory function that creates a graph adjacency index for relationship tracking (verbs/triples). Must implement the `GraphAdjacencyIndex` interface including `addVerb()`, `getVerbsBySource()`, `getVerbsByTarget()`, `flush()`, etc. ```typescript context.registerProvider('graphIndex', (storage) => { return new MyNativeGraphIndex(storage) }) ``` #### `aggregation` **Type:** `(storage: StorageAdapter) => AggregationProvider-compatible` Factory function that creates an aggregation engine for write-time incremental SUM/COUNT/AVG/MIN/MAX with GROUP BY and time windows. The returned object must implement the `AggregationProvider` interface. ```typescript context.registerProvider('aggregation', (storage) => { return new MyNativeAggregationEngine(storage) }) ``` When provided by an optional native acceleration plugin (such as `@soulcraft/cor`), this enables: - Compiled source filters (vs per-entity JS object traversal) - Precise MIN/MAX via sorted data structures (vs lazy recompute) - Parallel aggregate rebuild across CPU cores - SIMD-accelerated timestamp bucketing ### Utility Providers #### `cache` **Type:** `UnifiedCache` Replaces the global `UnifiedCache` singleton used for VFS path resolution, semantic caching, and vector index caching. Must implement the `UnifiedCache` interface (available from `@soulcraft/brainy/internals`). ```typescript import type { UnifiedCache } from '@soulcraft/brainy/internals' context.registerProvider('cache', myNativeCache) ``` #### `entityIdMapper` **Type:** `(storage: StorageAdapter) => EntityIdMapper-compatible` Factory for bidirectional UUID ↔ integer mapping used by roaring bitmaps. Must implement `getOrAssign()`, `getUuid()`, `getInt()`, `has()`, `remove()`, `flush()`, `clear()`. #### `roaring` **Type:** `RoaringBitmap32 class` Replacement for the roaring bitmap implementation. Used internally by the metadata index for set operations. Must be API-compatible with `roaring-wasm`. #### `msgpack` **Type:** `{ encode: (data: any) => Buffer, decode: (buffer: Buffer) => any }` Native msgpack encode/decode for SSTable serialization. ### Analytics Providers (Native-Only) These provider keys have **no JavaScript fallback** — they represent capabilities that require native code (SIMD, mmap, sub-microsecond latency). They are available when an optional native acceleration plugin (such as `@soulcraft/cor`) is installed. Use `brain.getProvider('analytics:hyperloglog')` to check availability. Returns `undefined` if no plugin provides it. #### `analytics:hyperloglog` Approximate distinct counts. Count unique values (e.g., unique merchants) across millions of records using ~16KB of memory with ~1% error. Each update is O(1). #### `analytics:tdigest` Streaming percentiles. Compute P50/P90/P95/P99 from streaming data without storing all values. Uses ~4KB per digest with ~1% accuracy at the tails. #### `analytics:countmin` Frequency estimation. Find the most common values (e.g., top-K merchants) using ~40KB with 0.1% error. O(1) per update. #### `analytics:anomaly` Real-time anomaly detection. Flag statistically unusual values at write-time using exponentially weighted moving averages. 64 bytes per group, sub-microsecond decisions. #### `aggregation:mmap` Persistent aggregate storage via memory-mapped files. Aggregate state survives process crashes without explicit flush. Zero serialization overhead. --- ## Storage Adapter Plugins Plugins can register custom storage backends that users reference by name. ### Implementing a Storage Adapter ```typescript import type { StorageAdapterFactory } from '@soulcraft/brainy/plugin' import type { StorageAdapter } from '@soulcraft/brainy' class MyStorageAdapter implements StorageAdapter { async init(): Promise { /* ... */ } async saveNoun(noun: HNSWNoun): Promise { /* ... */ } async getNoun(id: string): Promise { /* ... */ } async deleteNoun(id: string): Promise { /* ... */ } // ... implement all StorageAdapter methods } ``` ### Registering a Storage Adapter ```typescript context.registerProvider('storage:my-backend', { name: 'my-backend', create: (config: Record) => { return new MyStorageAdapter(config) } } satisfies StorageAdapterFactory) ``` Users can then use your storage: ```typescript const brain = new Brainy({ storage: 'my-backend', myBackendOption: 'value' }) ``` ## Import Paths Brainy provides three entry points for plugin developers: | Import Path | Contents | Stability | |-------------|----------|-----------| | `@soulcraft/brainy` | Public API, types, StorageAdapter | Stable (semver) | | `@soulcraft/brainy/plugin` | BrainyPlugin, BrainyPluginContext, StorageAdapterFactory | Stable (semver) | | `@soulcraft/brainy/internals` | UnifiedCache, EntityIdMapper, logger utilities | Internal (may change between minor versions) | ## Diagnostics Brainy provides a `diagnostics()` method to verify plugin wiring: ```typescript const brain = new Brainy() await brain.init() const diag = brain.diagnostics() console.log(diag) // { // version: '7.14.0', // plugins: { active: ['my-plugin'], count: 1 }, // providers: { // metadataIndex: { source: 'default' }, // graphIndex: { source: 'default' }, // embeddings: { source: 'plugin' }, // embedBatch: { source: 'plugin' }, // distance: { source: 'plugin' }, // vector: { source: 'default' }, // ... // }, // indexes: { // vector: { size: 0, type: 'JsHnswVectorIndex' }, // metadata: { type: 'MetadataIndexManager', initialized: true }, // graph: { type: 'GraphAdjacencyIndex', initialized: true, wiredToStorage: true } // } // } ``` The CLI also supports diagnostics: ```bash brainy diagnostics ``` ### Init-Time Summary When a plugin is active, brainy automatically logs a provider summary after `init()`: ``` [brainy] Plugin activated: @soulcraft/cor [brainy] Providers: 8/10 native (@soulcraft/cor) | default: vector, cache ``` This tells you at a glance how many subsystems are accelerated and which ones are falling back to JavaScript. The log respects `config.silent`. ### Fail-Fast for Production Use `requireProviders()` after `init()` to guarantee specific providers are plugin-supplied. This prevents silent fallback to JavaScript in deployments where you expect native acceleration: ```typescript const brain = new Brainy() await brain.init() // Throws immediately if any of these are using JS fallback brain.requireProviders(['distance', 'embeddings', 'metadataIndex', 'graphIndex']) ``` If a required provider is missing, the error message tells you exactly what's wrong: ``` [brainy] Required providers using JS fallback: graphIndex. Active plugins: @soulcraft/cor. These providers must be supplied by a plugin for this deployment. Check plugin installation, license, and native module availability. ``` This is the recommended pattern for production deployments with paid plugins — fail at startup rather than silently degrading performance. ## Complete Example: Distance Acceleration Plugin A minimal but useful plugin that provides SIMD-accelerated distance calculations: ```typescript // simd-distance-plugin/src/plugin.ts import type { BrainyPlugin, BrainyPluginContext } from '@soulcraft/brainy/plugin' // Hypothetical native module import { simdCosineDistance } from './native.js' const simdDistancePlugin: BrainyPlugin = { name: 'brainy-simd-distance', async activate(context: BrainyPluginContext): Promise { // Check if SIMD is available on this platform if (!checkSimdSupport()) { console.log('[simd-distance] SIMD not available, skipping') return false // Don't activate — brainy uses JS fallback } context.registerProvider('distance', simdCosineDistance) return true } } export default simdDistancePlugin ``` ```json // simd-distance-plugin/package.json { "name": "brainy-simd-distance", "main": "./dist/plugin.js", "types": "./dist/plugin.d.ts", "peerDependencies": { "@soulcraft/brainy": ">=7.0.0" } } ``` Usage: ```typescript import { Brainy } from '@soulcraft/brainy' const brain = new Brainy({ plugins: ['brainy-simd-distance'] }) await brain.init() // Verify it's active const diag = brain.diagnostics() console.log(diag.providers.distance) // { source: 'plugin' } ``` ## Design Principles 1. **Brainy works perfectly without plugins.** Every provider has a JavaScript fallback. Plugins only improve performance or add capabilities. 2. **Provider keys are string-based.** The plugin system is not coupled to any specific plugin. Any package can register any provider. 3. **Clean separation.** Plugins access brainy through the documented `BrainyPluginContext` interface. No direct access to internal classes is needed. 4. **Fail-safe activation.** If a plugin throws during `activate()`, brainy logs a warning and continues with defaults. A broken plugin never prevents brainy from working. 5. **Lifecycle management.** `deactivate()` is called during `brainy.close()` for resource cleanup. Native resources, connections, and file handles should be released here.