Overview
Last updated April 15, 2026
What adapters are, why you'd swap one, and how.
What an adapter is
An adapter is a concrete implementation of a platform interface. Every backend KB Labs talks to — the LLM, cache, storage, vector store, logger, database — is an adapter package. Plugins never import adapters directly. They call useLLM(), useCache(), useStorage() and get whatever adapter your kb.config.jsonc points at.
The runtime depends on the interface (ILLM, ICache, IStorage, …). The adapter provides the implementation. Swap the package, the interface stays identical, plugin code doesn't change.
Why you'd swap
- Change LLM provider. Move from the default KB Labs Gateway to your own OpenAI key, or to a self-hosted inference server. See Guides → LLM Setup.
- Persist logs instead of stdout. Default
@kb-labs/adapters-pinoprints to stdout. Swap to@kb-labs/adapters-log-sqlitefor a searchable local database of every structured log line. - Use an external vector store. Default configuration is fine for small workspaces. Swap to
@kb-labs/adapters-qdrantwhen you want a persistent Qdrant deployment shared across machines. - Pick your analytics sink.
@kb-labs/adapters-analytics-file(JSONL, zero deps) →@kb-labs/adapters-analytics-sqliteoranalytics-duckdbwhen you want to query events with SQL. - Disable a capability. Set an adapter to
nullto explicitly bind the NoOp implementation (e.g., disable analytics in dev:"analytics": null).
How you swap
One line per adapter in .kb/kb.config.jsonc:
{
"platform": {
"adapters": {
"llm": "@kb-labs/adapters-openai", // was: "@kb-labs/adapters-kblabs-gateway"
"vectorStore": "@kb-labs/adapters-qdrant",
"analytics": "@kb-labs/adapters-analytics-sqlite"
},
"adapterOptions": {
"llm": { "model": "gpt-4o-mini" }, // reads OPENAI_API_KEY from env
"vectorStore": { "url": "http://localhost:6333" }
}
}
}Environment variable substitution is supported — write "apiKey": "${MY_API_KEY}" and the loader resolves it at startup (throws if the var is missing). Then kb-dev stop && kb-dev start to reload. No plugin rebuild, no code change.
For worked examples: Guides → LLM Setup covers the LLM-specific walkthrough; Built-in adapters lists every adapter that ships with the platform; Guides → First Adapter shows how to write your own.
The separation
┌─────────────────────────────────────────────────────┐
│ Plugin code │
│ │
│ const llm = useLLM(); │
│ const cache = useCache(); │
│ const store = useStorage(); │
└────────────────────┬────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────┐
│ Core-platform interfaces │
│ │
│ ILLM, ICache, IStorage, ILogger, IVectorStore, │
│ IEmbeddings, IAnalytics, IEventBus, ISQLDatabase, │
│ IDocumentDatabase, IKeyValueDatabase, ... │
└────────────────────┬────────────────────────────────┘
▼
┌──────────────────────────────────────────────────────────────┐
│ Adapters │
│ │
│ @kb-labs/adapters-openai (implements ILLM) │
│ @kb-labs/adapters-redis (implements ICache) │
│ @kb-labs/adapters-fs (implements IStorage) │
│ @kb-labs/adapters-pino (implements ILogger) │
│ @kb-labs/adapters-qdrant (implements IVectorStore) │
│ @kb-labs/adapters-analytics-sqlite (implements IAnalytics)│
│ ... │
└──────────────────────────────────────────────────────────────┘The interfaces live in @kb-labs/core-platform. The adapters live in adapters/. Plugin code never imports adapter packages directly — it goes through the runtime hooks, which return whatever adapter the user configured.
Declaration: kb.config.json
You choose adapters by listing their packages in platform.adapters:
{
"platform": {
"adapters": {
"llm": "@kb-labs/adapters-openai",
"cache": "@kb-labs/adapters-redis",
"storage": "@kb-labs/adapters-fs",
"vectorStore": "@kb-labs/adapters-qdrant",
"logger": "@kb-labs/adapters-pino",
"analytics": "@kb-labs/adapters-analytics-sqlite"
},
"adapterOptions": {
"cache": { "url": "redis://localhost:6379" },
"storage": { "basePath": ".kb/storage" }
}
}
}Each key in platform.adapters is a runtime token — the well-known name the runtime binds the adapter to (llm, cache, ...). The value is the npm package path that exports the adapter. The options under platform.adapterOptions[token] are passed verbatim to the adapter's createAdapter(config, deps) factory.
See Configuration → kb.config.json for the full schema and every supported token.
Anatomy of an adapter package
An adapter package exports two things: a manifest and a factory.
// @kb-labs/adapters-pino/src/index.ts
import type { AdapterManifest, AdapterFactory, ILogger, IAnalytics } from '@kb-labs/core-platform';
export const manifest: AdapterManifest = {
manifestVersion: '1.0.0',
id: 'pino-logger',
name: 'Pino Logger',
version: '1.0.0',
type: 'core',
implements: 'ILogger',
optional: { adapters: ['analytics'] },
capabilities: { streaming: true },
};
export const createAdapter: AdapterFactory<PinoConfig, { analytics?: IAnalytics }, ILogger> = (
config,
deps,
) => {
return new PinoAdapter(config, deps);
};The runtime imports the package, reads the manifest, and calls createAdapter(config, deps) with:
config— the value fromplatform.adapterOptions[token]inkb.config.json.deps— an object populated with any dependencies declared in the manifest.
The return value is the adapter instance. It must implement the interface named in manifest.implements.
The manifest, in detail
Source of truth: adapter-manifest.ts.
Identity
{
manifestVersion: '1.0.0', // schema version for future migrations
id: 'pino-logger', // unique, kebab-case
name: 'Pino Logger', // human-readable
version: '1.0.0', // semver
description?: string,
author?: string,
license?: string, // SPDX identifier
homepage?: string,
}Classification
{
type: 'core' | 'extension' | 'proxy',
implements: 'ILogger', // interface name — informational only
}core— primary adapter implementing an interface. Most adapters are core.extension— adapter that extends another via a hook (see below). Think "log ring buffer extending the logger".proxy— adapter that wraps/delegates to another adapter. Used by the IPC layer for cross-process communication.
implements is informational — the runtime doesn't do runtime type checking against it. TypeScript provides compile-time guarantees in the factory's return type.
Dependencies
{
requires?: {
adapters?: (string | { id: string; alias?: string })[];
platform?: string; // semver range, e.g. '>= 1.0.0'
};
optional?: {
adapters?: string[];
};
contexts?: string[]; // 'workspace' | 'analytics' | 'tenant' | 'request'
}Runtime tokens, not manifest IDs. When log-persistence declares requires.adapters: ['db'], it means "the adapter bound to the db token in config", not "the adapter whose manifest ID is db". This is called out loudly in the source — it's the easy thing to get wrong when writing dependent adapters.
Required dependencies that can't be resolved cause the adapter to fail loading. Optional dependencies resolve to undefined in the deps parameter if missing, and the adapter continues.
Contexts are runtime-injected values the loader threads into the adapter's config:
workspace—{ cwd: string }analytics—AnalyticsContextwith source, actor, and runIdtenant/request— reserved for future multi-tenancy and per-request scoping
Extension points
Extensions are how one adapter augments another without tight coupling.
{
type: 'extension',
implements: 'ILogRingBuffer',
extends: {
adapter: 'logger', // target runtime token
hook: 'onLog', // method on target that accepts a callback
method: 'append', // method on this adapter to invoke
priority: 10, // higher = called first; default 0
},
}After all adapters load, the runtime walks every extension, finds the target adapter, and registers target[hook](event => this[method](event)). Multiple extensions can hook the same target; they fire in priority order, then in registration order.
This is how @kb-labs/adapters-log-ringbuffer and @kb-labs/adapters-log-sqlite both plug into the Pino logger at the same time: both declare extends: { adapter: 'logger', hook: 'onLog', ... } with different methods and priorities. The logger itself knows nothing about them — it just calls onLog(record) for each log entry, and the hook dispatcher delivers.
Capabilities
{
capabilities?: {
streaming?: boolean;
batch?: boolean;
search?: boolean;
transactions?: boolean;
custom?: Record<string, unknown>;
};
}Informational flags that the runtime (or plugin code) can read to branch on. For example, an LLM adapter with capabilities.streaming === false will cause useLLM().stream() to fall back to complete() unless the caller explicitly requires streaming.
Config schema
configSchema?: Record<string, {
type: 'string' | 'number' | 'boolean' | 'object' | 'array';
description?: string;
default?: unknown;
required?: boolean;
enum?: unknown[];
properties?: Record<string, unknown>;
}>;Purely informational — used for documentation generation, IDE completion, config builders. The runtime does not validate adapter config against this schema; adapters validate their own inputs.
Loading order
Adapters are loaded in dependency order using Kahn's topological sort. The process, straight from adapter-loader.ts:
- For every entry in
platform.adapters, dynamically import the package and pull out itsmanifest+createAdapter. - Build a dependency graph: every
requires.adaptersentry becomes an edge from the dependency to the dependent. - Topologically sort the graph. Circular dependencies throw at startup.
- For each node in sorted order:
- Resolve its deps into an object keyed by token (or by the
aliasif one was declared). - Inject any requested contexts into the config.
- Call
createAdapter(config, deps). - Register the resulting instance under its runtime token.
- Resolve its deps into an object keyed by token (or by the
- After all core adapters are in place, process every
extensionsentry and wire hooks.
The end result is a global platform singleton with every adapter attached at its well-known token. This is what usePlatform() and the useLLM()/useCache()/etc. hooks read from.
Multi-adapter setups
platform.adapters[token] accepts a single string, an array of strings, or null:
{
"platform": {
"adapters": {
"llm": ["@kb-labs/adapters-openai", "@kb-labs/adapters-vibeproxy"],
"analytics": null
}
}
}- Single string — the normal case. One adapter per token.
- Array — multi-provider setups. The first entry is the primary/default, the rest are reachable via routing. For LLM, the router reads
adapterOptions.llm.tierMappingto decide which adapter to use per tier. null— explicitly install the NoOp adapter for that token. The hook will still be defined, but every operation is a silent no-op. Useful when you want the runtime to know the service is deliberately disabled.- Omitted — service is simply not configured. The corresponding hook returns
undefined, and plugins degrade gracefully.
NoOp adapters
Every interface has a matching NoOp implementation under @kb-labs/core-platform/noop. NoOp adapters let the runtime start even when nothing is configured — operations succeed silently and data is discarded.
This is deliberate: it lets you develop plugins without a full infrastructure stack running.
First-party adapter catalog
The monorepo ships these adapters under adapters/:
| Token | Package | Interface |
|---|---|---|
llm | @kb-labs/adapters-openai | ILLM |
llm | @kb-labs/adapters-vibeproxy | ILLM |
embeddings | @kb-labs/adapters-openai/embeddings | IEmbeddings |
vectorStore | @kb-labs/adapters-qdrant | IVectorStore |
cache | @kb-labs/adapters-redis | ICache |
storage | @kb-labs/adapters-fs | IStorage |
logger | @kb-labs/adapters-pino | ILogger |
logRingBuffer | @kb-labs/adapters-log-ringbuffer | extends logger |
logPersistence | @kb-labs/adapters-log-sqlite | extends logger |
analytics | @kb-labs/adapters-analytics-sqlite | IAnalytics |
analytics | @kb-labs/adapters-analytics-file | IAnalytics |
analytics | @kb-labs/adapters-analytics-duckdb | IAnalytics |
eventBus | @kb-labs/adapters-eventbus-cache | IEventBus |
db | @kb-labs/adapters-sqlite | ISQLDatabase |
documentDb | @kb-labs/adapters-mongodb | IDocumentDatabase |
workspace | @kb-labs/adapters-workspace-localfs | IWorkspaceProvider |
workspace | @kb-labs/adapters-workspace-worktree | IWorkspaceProvider |
workspace | @kb-labs/adapters-workspace-agent | IWorkspaceProvider |
environment | @kb-labs/adapters-environment-docker | IEnvironmentProvider |
snapshot | @kb-labs/adapters-snapshot-localfs | ISnapshotProvider |
Every adapter lives in its own package under adapters/. Each package has a README covering its specific config options.
Writing a custom adapter
See Writing a Custom Adapter for the step-by-step. The short version:
- Pick an interface from
@kb-labs/core-platform— whichever one you're implementing. - Create an npm package that depends on
@kb-labs/core-platform. - Export
manifest: AdapterManifestandcreateAdapter: AdapterFactory. - Implement the interface in the factory return value.
- Publish the package (or link it locally with
kb marketplace link). - Add it to
platform.adapters[token]inkb.config.json.
The platform doesn't care whether the package comes from npm, a local monorepo, or kb marketplace link. As long as it exports the two contract symbols and the factory returns an object that quacks like the interface, it works.
What's NOT in an adapter
Adapters are thin. They translate platform interface calls into vendor-specific API calls, and that's it. Things that don't belong in an adapter:
- Plugin logic. An LLM adapter calls the OpenAI API; it doesn't know how to write commit messages. Commit-message generation lives in a plugin.
- Permission enforcement. Permissions are enforced by the runtime sandbox (
ctx.runtime.fs,ctx.runtime.env, ...), not by adapters. The storage adapter doesn't know aboutpermissions.platform.storage— the runtime intercepts writes before they reach the adapter. - Analytics attribution. Adapters emit raw events; the platform's
analyticswrapper attributes them to the current plugin viasetSource(). Don't hard-code a product name in your adapter. - Cross-cutting concerns. Rate limiting, retries, and cache policy live in the
core.resourceBrokerlayer and in per-adapter wrappers. Adapters should trust they'll be wrapped.
What to read next
- Interfaces → Overview — the per-interface reference pages.
- Writing a Custom Adapter — step-by-step tutorial.
- Configuration → kb.config.json — the full config schema.
- SDK → Hooks — how plugins consume adapter interfaces.