Overview
Last updated April 7, 2026
Interface-first adapters: how they're declared, loaded, wired, and extended.
KB Labs is built interface-first. The runtime depends on a small set of TypeScript interfaces — ILLM, ICache, IStorage, ILogger, and so on — and adapters are the concrete implementations of those interfaces. Every backend the platform talks to (OpenAI, Qdrant, Redis, SQLite, Pino, Docker) is an adapter package. Swapping one for another is a one-line change in kb.config.json.
This page covers how adapters are declared, loaded, and wired together. The per-interface reference pages under Adapters → Service Interfaces document each interface's method surface.
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 infra/kb-labs-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 infra/kb-labs-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 |
db | @kb-labs/adapters-postgres | 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 infra/kb-labs-adapters/packages/. 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.