Adapter System
Обновлено 16 мая 2026 г.
What adapters are and why the platform uses them.
Adapters implement the concrete backends that the KB Labs platform depends on: LLM providers, caches, vector stores, databases, loggers, and more. You choose which adapters to use in kb.config.json. Plugins never reference adapters directly — they call useLLM(), useCache(), useStorage(), and the platform injects whatever is configured.
This page explains the design. For hands-on usage see Guides → Writing Your First Adapter.
Interface-first design
Every platform capability has a TypeScript interface that defines the contract:
ILLM—chat(),chatWithTools(),embed(),countTokens()ICache—get(),set(),delete(),clear()IVectorStore—upsert(),search(),delete(),createCollection()IStorage—read(),write(),delete(),list(),exists()ILogger—debug(),info(),warn(),error(),child()IAnalytics—track(),identify(),flush()IEnvironmentAdapter— provision, start, attach, release sandbox environments
These interfaces live in @kb-labs/core-platform. Adapters live in separate packages and implement one or more interfaces. The platform runtime holds references to the interface type, not the adapter class — which is how you can swap OpenAI for Anthropic without touching any plugin code.
Shipped adapters
The platform ships adapters for common backends out of the box:
| Interface | Adapter | Package |
|---|---|---|
ILLM | OpenAI (GPT-4o, o1, ...) | @kb-labs/adapter-llm-openai |
ILLM | Anthropic (Claude 3.x, 4.x) | @kb-labs/adapter-llm-anthropic |
ICache | In-memory | @kb-labs/adapter-cache-memory |
ICache | Redis | @kb-labs/adapter-cache-redis |
IVectorStore | Qdrant | @kb-labs/adapter-vector-qdrant |
IVectorStore | In-memory | @kb-labs/adapter-vector-memory |
IStorage | Local filesystem | @kb-labs/adapter-storage-fs |
ILogger | Console (structured JSON) | @kb-labs/adapter-logger-console |
IEnvironmentAdapter | Docker | @kb-labs/adapter-environment-docker |
In-memory adapters (adapter-cache-memory, adapter-vector-memory) are the default in dev — no external services required. For production you swap to Redis and Qdrant by changing two lines in kb.config.json.
How adapters are loaded
The platform reads .kb/kb.config.json at startup. The adapters section maps each interface to a package reference and optional configuration:
{
"adapters": {
"llm": {
"package": "@kb-labs/adapter-llm-openai",
"options": {
"model": "gpt-4o",
"temperature": 0.2
}
},
"cache": {
"package": "@kb-labs/adapter-cache-redis",
"options": {
"url": "redis://localhost:6379"
}
},
"vectorStore": {
"package": "@kb-labs/adapter-vector-qdrant",
"options": {
"url": "http://localhost:6333"
}
}
}
}At startup loadPlatformConfig() reads this, dynamically imports each adapter package, instantiates the adapter with its options, and registers it in the platform singleton. From that point on, useLLM() returns the OpenAI adapter, useCache() returns Redis, and so on — regardless of which plugin or service is calling.
Adapters vs plugins
This distinction trips people up at first:
| Adapter | Plugin | |
|---|---|---|
| What it is | Concrete backend for a platform interface | Feature contributed to the platform |
| Examples | OpenAI LLM, Redis cache, Qdrant vector store | Commit assistant, QA runner, AI review |
| How it's configured | kb.config.json → adapters.* | kb marketplace install |
| Who uses it | The platform runtime, other adapters | End users, CI pipelines |
| Sandbox | No — adapters run with host-level access | Yes — always runs in the plugin sandbox |
If you want to change what LLM provider the platform uses, that's an adapter. If you want to add a new AI-powered command, that's a plugin. A plugin calls useLLM() and is blissfully unaware of whether it ends up talking to OpenAI or Anthropic.
Middleware pipeline
Before a plugin ever calls useLLM() or useCache(), every adapter passes through a named slot pipeline. The pipeline adds routing, rate limiting, and permission enforcement in a fixed, auditable order.
raw → router → post-router → resource-broker → post-resource-broker → governance| Slot | Plugins can use | What runs here |
|---|---|---|
raw | yes | Before any system processing |
router | no | LLMRouter (backend selection), NotifierRouter |
post-router | yes | After routing, before rate limiting |
resource-broker | no | QueuedLLM, concurrency limits |
post-resource-broker | yes | Cost tracking, circuit breaker, custom instrumentation |
governance | no | Permission enforcement — always the last stage |
The pipeline runs in two phases:
- Phase 1 —
assemblePlatform()— once at startup. Applies router factories (LLMRouter wraps the raw LLM client for backend selection) and resource broker factories (QueuedLLM wraps for rate limiting). - Phase 2 —
applyPluginGovernance()— once per plugin. Applies any adapter-declared middlewares in slot order, then runs system governance last to enforce the plugin'splatform.*permissions.
Adapter middleware
Adapter packages can declare middleware in their AdapterManifest. A middleware receives the current adapter and returns a new one — the classic wrap pattern:
// adapters/my-llm/middlewares/cost-tracker.ts
import type { AdapterMiddlewareFn } from '@kb-labs/plugin-runtime/platform';
export const middleware: AdapterMiddlewareFn<ILLM> = (adapter, ctx) => ({
...adapter,
complete: async (prompt, options) => {
const start = Date.now();
const result = await adapter.complete(prompt, options);
recordCost(Date.now() - start, ctx.pluginId);
return result;
},
});Declare it in the manifest, targeting an open slot:
middlewares: [
{
id: 'cost-tracker',
handler: './middlewares/cost-tracker.js',
slot: 'post-resource-broker',
target: 'llm',
priority: 10, // local within the slot, not global
}
]priority is local to the slot. Adding a new system stage never shifts existing priority numbers.
Single source of truth
Every adapter has exactly one entry in ADAPTER_REGISTRY (in core/plugin-runtime). TypeScript enforces exhaustiveness: adding a field to PlatformServices without a registry entry is a compile error. Each entry declares a governance strategy ('wrap' with a function, or 'pass-through') and an ipc strategy ('proxy', 'noop', 'local', or 'absent'). There is no separate list to keep in sync.
Platform transport adapters
When plugins run outside the main process (worker pool, subprocess, container), they can't access platform services directly — the real adapters live in the parent process. The platform solves this with platform transport adapters: a pluggable IPC layer that forwards adapter calls across process boundaries.
| Interface | Transport | Package / Module | Use case |
|---|---|---|---|
ITransport | Node.js fork IPC | @kb-labs/core-ipc (IPCTransport) | Worker pool (default) |
ITransport | Unix socket / Windows named pipe | @kb-labs/core-ipc (UnixSocketTransport) | Subprocess mode |
ITransport | Gateway WebSocket | Roadmap | Remote / container execution |
How it works
Each execution mode has two sides:
- Parent side — a
PlatformTransportServerthat listens foradapter:callmessages and dispatches them to real adapters. For worker pool this isChildIPCServer; for subprocess it'sUnixSocketServer. - Child side — an
ITransportimplementation that sendsadapter:calland awaitsadapter:response. For worker pool this isIPCTransport(viaprocess.send); for subprocess it'sUnixSocketTransport.
The child creates proxy adapters (LLMProxy, CacheProxy, etc.) on top of the transport. Plugin code calls useLLM().complete(...), which goes through the proxy → transport → parent → real adapter → back. The plugin never knows it's crossing a process boundary.
Configuring transport
The transport is selected automatically based on execution mode. To override, pass a custom PlatformTransportFactory via BackendOptions.platformTransport:
import { createExecutionBackend } from '@kb-labs/plugin-execution-factory';
const backend = createExecutionBackend({
platform,
mode: 'worker-pool',
platformTransport: myCustomTransportFactory, // optional override
});The factory interface:
interface PlatformTransportFactory {
readonly type: string; // e.g. 'ipc', 'unix-socket', 'grpc'
createServer(platform: PlatformServices, child: ChildProcess): PlatformTransportServer;
getChildEnv?(): Record<string, string>; // extra env vars for child process
}The type is passed to the child via KB_PLATFORM_TRANSPORT env var. The child process looks it up and creates the matching client transport. Built-in types: ipc (default), unix-socket.
Cross-platform support
Socket paths are generated via createSocketPath(id) which returns:
- Unix/macOS:
/tmp/kb-{id}.sock(Unix domain socket) - Windows:
\\?\pipe\kb-{id}(Named pipe)
Node.js net API handles both transparently — no code changes between platforms.
Permission enforcement
Platform transport includes two layers of permission enforcement:
- Layer 1 (child side) —
createGovernedPlatformServices()wraps the proxy platform with permission checks. Calls to denied services throwPermissionErrorimmediately, no IPC roundtrip. - Layer 2 (parent side) —
ChildIPCServerreadscall.context.permissionsfrom each incomingadapter:calland rejects denied adapters before dispatching. Defense in depth — can't be bypassed even if child code is compromised.
Writing a custom transport
Implement PlatformTransportFactory and pass it to BackendOptions.platformTransport. Your factory creates the server side; the child side needs a matching ITransport implementation. Register the child-side transport type in worker-script.ts's createTransport() switch, or use KB_PLATFORM_TRANSPORT_MODULE for dynamic import (roadmap).
Writing a custom adapter
Implement the relevant interface, export a factory function, and reference your package in kb.config.json:
// my-custom-cache/src/index.ts
import type { ICache } from '@kb-labs/core-platform';
export function createAdapter(options: { url: string }): ICache {
return {
async get(key) { /* ... */ },
async set(key, value, ttlMs) { /* ... */ },
async delete(key) { /* ... */ },
async clear() { /* ... */ },
};
}{
"adapters": {
"cache": {
"package": "./my-custom-cache",
"options": { "url": "..." }
}
}
}The platform calls createAdapter(options) and uses the returned object for all cache operations. Your adapter doesn't need to register anywhere or extend a base class — the interface contract is the only requirement.
See Guides → Writing Your First Adapter for a step-by-step walkthrough.
What to read next
- Configuration → kb.config.json — full reference for the adapters section.
- Plugin System — the other extension point (features, not backends).
- Execution Model — how the platform provisions sandboxed environments (uses
IEnvironmentAdapterinternally). - Guides → Writing Your First Adapter — hands-on walkthrough.