KB LabsDocs

Adapter System

Last updated May 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:

  • ILLMchat(), chatWithTools(), embed(), countTokens()
  • ICacheget(), set(), delete(), clear()
  • IVectorStoreupsert(), search(), delete(), createCollection()
  • IStorageread(), write(), delete(), list(), exists()
  • ILoggerdebug(), info(), warn(), error(), child()
  • IAnalyticstrack(), 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:

InterfaceAdapterPackage
ILLMOpenAI (GPT-4o, o1, ...)@kb-labs/adapter-llm-openai
ILLMAnthropic (Claude 3.x, 4.x)@kb-labs/adapter-llm-anthropic
ICacheIn-memory@kb-labs/adapter-cache-memory
ICacheRedis@kb-labs/adapter-cache-redis
IVectorStoreQdrant@kb-labs/adapter-vector-qdrant
IVectorStoreIn-memory@kb-labs/adapter-vector-memory
IStorageLocal filesystem@kb-labs/adapter-storage-fs
ILoggerConsole (structured JSON)@kb-labs/adapter-logger-console
IEnvironmentAdapterDocker@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:

JSON
{
  "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:

AdapterPlugin
What it isConcrete backend for a platform interfaceFeature contributed to the platform
ExamplesOpenAI LLM, Redis cache, Qdrant vector storeCommit assistant, QA runner, AI review
How it's configuredkb.config.jsonadapters.*kb marketplace install
Who uses itThe platform runtime, other adaptersEnd users, CI pipelines
SandboxNo — adapters run with host-level accessYes — 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
SlotPlugins can useWhat runs here
rawyesBefore any system processing
routernoLLMRouter (backend selection), NotifierRouter
post-routeryesAfter routing, before rate limiting
resource-brokernoQueuedLLM, concurrency limits
post-resource-brokeryesCost tracking, circuit breaker, custom instrumentation
governancenoPermission 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's platform.* 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:

TypeScript
// 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:

TypeScript
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.

InterfaceTransportPackage / ModuleUse case
ITransportNode.js fork IPC@kb-labs/core-ipc (IPCTransport)Worker pool (default)
ITransportUnix socket / Windows named pipe@kb-labs/core-ipc (UnixSocketTransport)Subprocess mode
ITransportGateway WebSocketRoadmapRemote / container execution

How it works

Each execution mode has two sides:

  • Parent side — a PlatformTransportServer that listens for adapter:call messages and dispatches them to real adapters. For worker pool this is ChildIPCServer; for subprocess it's UnixSocketServer.
  • Child side — an ITransport implementation that sends adapter:call and awaits adapter:response. For worker pool this is IPCTransport (via process.send); for subprocess it's UnixSocketTransport.

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:

TYPESCRIPT
import { createExecutionBackend } from '@kb-labs/plugin-execution-factory';
 
const backend = createExecutionBackend({
  platform,
  mode: 'worker-pool',
  platformTransport: myCustomTransportFactory, // optional override
});

The factory interface:

TYPESCRIPT
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:

  1. Layer 1 (child side)createGovernedPlatformServices() wraps the proxy platform with permission checks. Calls to denied services throw PermissionError immediately, no IPC roundtrip.
  2. Layer 2 (parent side)ChildIPCServer reads call.context.permissions from each incoming adapter:call and 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:

TYPESCRIPT
// 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() { /* ... */ },
  };
}
JSON
{
  "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.

Adapter System — KB Labs Docs