# Provider Adapter Layer — BYOAI Adapters & Registries

> The pluggable adapter registry (src/lib/agents/adapters/) that maps provider IDs to concrete CLI-backed adapters: claude-local, codex-local, cursor-local, gemini-local, opencode-local, pi-local, grok-local, and copilot-local. Documents the AgentAdapter interface, per-adapter stream parsers, the plugin-loader for third-party adapters, provider-registry resolution, and how per-run provider/model overrides are applied at launch time.

- Repository: hilash/cabinet
- GitHub: https://github.com/hilash/cabinet
- Human wiki: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59
- Complete Markdown: https://grok-wiki.com/public/wiki/hilash-cabinet-73c70f449a59/llms-full.txt

## Source Files

- `src/lib/agents/adapters/index.ts`
- `src/lib/agents/adapters/types.ts`
- `src/lib/agents/adapters/claude-local.ts`
- `src/lib/agents/adapters/codex-local.ts`
- `src/lib/agents/adapters/plugin-loader.ts`
- `src/lib/agents/provider-registry.ts`
- `src/lib/agents/provider-runtime.ts`
- `src/lib/agents/providers/claude-code.ts`

---

<details>
<summary>Relevant source files</summary>

The following files were used as context for generating this wiki page:

- [src/lib/agents/adapters/types.ts](src/lib/agents/adapters/types.ts)
- [src/lib/agents/adapters/registry.ts](src/lib/agents/adapters/registry.ts)
- [src/lib/agents/adapters/index.ts](src/lib/agents/adapters/index.ts)
- [src/lib/agents/adapters/plugin-loader.ts](src/lib/agents/adapters/plugin-loader.ts)
- [src/lib/agents/adapters/claude-local.ts](src/lib/agents/adapters/claude-local.ts)
- [src/lib/agents/adapters/codex-local.ts](src/lib/agents/adapters/codex-local.ts)
- [src/lib/agents/adapters/grok-local.ts](src/lib/agents/adapters/grok-local.ts)
- [src/lib/agents/adapters/copilot-local.ts](src/lib/agents/adapters/copilot-local.ts)
- [src/lib/agents/adapters/pi-local.ts](src/lib/agents/adapters/pi-local.ts)
- [src/lib/agents/adapters/opencode-local.ts](src/lib/agents/adapters/opencode-local.ts)
- [src/lib/agents/adapters/cursor-local.ts](src/lib/agents/adapters/cursor-local.ts)
- [src/lib/agents/adapters/error-classification.ts](src/lib/agents/adapters/error-classification.ts)
- [src/lib/agents/adapters/utils.ts](src/lib/agents/adapters/utils.ts)
- [src/lib/agents/provider-registry.ts](src/lib/agents/provider-registry.ts)
- [src/lib/agents/provider-runtime.ts](src/lib/agents/provider-runtime.ts)
- [src/lib/agents/providers/claude-code.ts](src/lib/agents/providers/claude-code.ts)
</details>

# Provider Adapter Layer — BYOAI Adapters & Registries

Cabinet's provider adapter layer is the seam between the job runner and every supported AI CLI tool. Rather than wiring the runner directly to any one tool, it routes each run through an `AgentExecutionAdapter` whose `type` key is stored with the run. A dedicated registry maps those keys to concrete adapter objects, and a separate plugin loader lets users drop in third-party adapters at runtime without touching source code.

The layer has two complementary registries: a **provider registry** (`src/lib/agents/provider-registry.ts`) that describes what tools are available and how to check their health, and an **adapter registry** (`src/lib/agents/adapters/registry.ts`) that describes how to actually execute a run against those tools. Providers are stable identifiers; adapters are interchangeable execution strategies that reference providers by `providerId`.

---

## Architecture Overview

```text
┌─────────────────────────────────────────────────────────┐
│                   Job Runner / Daemon                   │
│   resolves adapterType ──► agentAdapterRegistry.get()   │
└──────────────────────────┬──────────────────────────────┘
                           │
          ┌────────────────▼────────────────┐
          │       AgentAdapterRegistry       │   registry.ts
          │  register()  registerExternal()  │
          └────────┬─────────────────────────┘
                   │
   ┌───────────────┼───────────────────────────────────────┐
   │               │  built-in adapters                    │
   │  claude_local │ codex_local │ gemini_local │ pi_local  │
   │  cursor_local │ opencode_local │ grok_local            │
   │  copilot_local │ *_legacy (PTY) ×8                     │
   └───────────────┼───────────────────────────────────────┘
                   │ external plugins
          ┌────────▼────────────────┐
          │   plugin-loader.ts      │  ~/.cabinet/adapter-plugins.json
          │ loadExternalAdapters()  │
          └─────────────────────────┘

   ProviderRegistry (provider-registry.ts)
   ├── claude-code  ├── codex-cli   ├── gemini-cli
   ├── cursor-cli   ├── opencode    ├── pi
   ├── grok-cli     └── copilot-cli
```

---

## `AgentExecutionAdapter` Interface

Every adapter — built-in or third-party — must satisfy `AgentExecutionAdapter` defined in `src/lib/agents/adapters/types.ts`.

| Field | Type | Purpose |
|---|---|---|
| `type` | `string` | Stable key stored with the run (e.g. `"claude_local"`) |
| `name` | `string` | Human-readable label shown in the UI |
| `providerId` | `string?` | Links back to the `ProviderRegistry` entry |
| `executionEngine` | `AgentAdapterExecutionEngine` | One of `"structured_cli"`, `"legacy_pty_cli"`, `"api"`, `"http"`, `"process"` |
| `supportsSessionResume` | `boolean?` | Whether the adapter can resume a prior conversation |
| `supportsDetachedRuns` | `boolean?` | Whether the adapter can run without an attached terminal |
| `models` | `AgentAdapterModel[]?` | Provider-backed model list, forwarded to the UI picker |
| `effortLevels` | `AgentAdapterEffortLevel[]?` | Configurable reasoning depth labels |
| `sessionCodec` | `AdapterSessionCodec?` | Serialize / deserialize opaque session state |
| `testEnvironment(ctx?)` | `Promise<AdapterEnvironmentTestResult>` | **Required.** Reports health (pass/warn/fail) + per-check hints |
| `execute(ctx)` | `Promise<AdapterExecutionResult>?` | **Optional** on legacy PTY adapters; required for structured execution |
| `classifyError(stderr, exitCode)` | `ConversationErrorClassification?` | Maps failure output to a canonical error kind for UI remediation |

Sources: [src/lib/agents/adapters/types.ts:85-130]()

### `AdapterExecutionContext`

The runner passes a single context object to `execute()`:

| Field | Notes |
|---|---|
| `runId` | Unique ID of the job, used by Pi adapter for session-file naming |
| `adapterType` | Resolved adapter type string |
| `config` | Opaque `Record<string, unknown>` carrying model, effort, systemPrompt, skillsDir, etc. |
| `prompt` | The full prompt text piped as stdin or passed as a positional arg |
| `cwd` | Working directory for the child process |
| `timeoutMs` | Optional kill deadline |
| `sessionId` / `sessionParams` | Resume handles, codec-deserialized before the call |
| `onLog(stream, chunk)` | Streaming callback for live log display |
| `onMeta(meta)` | Pre-launch metadata: command, args, env |
| `onSpawn(meta)` | Post-fork metadata: PID, process-group ID, started timestamp |

Sources: [src/lib/agents/adapters/types.ts:23-43]()

### `AdapterExecutionResult`

Each successful or failed `execute()` returns:

| Field | Notes |
|---|---|
| `exitCode` / `signal` / `timedOut` | Raw process termination status |
| `usage` | `inputTokens`, `outputTokens`, `cachedInputTokens` when the adapter can parse them |
| `sessionId` / `sessionParams` / `sessionDisplayId` | Codec-ready resume handles |
| `provider` / `model` | Actual provider/model reported by the run |
| `billingType` | `"api"` \| `"subscription"` \| `"metered_api"` \| `"credits"` \| `"unknown"` |
| `summary` / `output` | First non-empty output line and full assistant text |
| `errorMessage` / `errorCode` | On failure, human-readable message + structured code |
| `clearSession` | Signal to the runner to discard the stored session |

Sources: [src/lib/agents/adapters/types.ts:44-70]()

---

## Adapter Registry

`AgentAdapterRegistry` (`registry.ts`) is a singleton that owns the in-process map of adapter type → adapter object.

```typescript
// src/lib/agents/adapters/registry.ts (simplified)
class AgentAdapterRegistry {
  adapters = new Map<string, AgentExecutionAdapter>();
  private builtinFallbacks = new Map<string, AgentExecutionAdapter>();
  defaultAdapterType = claudeLocalAdapter.type; // "claude_local"

  register(adapter)          // built-in, no fallback tracking
  registerExternal(adapter)  // saves the displaced built-in as a fallback
  unregisterExternal(type)   // restores the built-in fallback
  get(type)
  listAll()
  findByProviderId(providerId)
}
export const agentAdapterRegistry = new AgentAdapterRegistry();
```

When `registerExternal` displaces an existing built-in with the same `type`, the old adapter is preserved in `builtinFallbacks`. `unregisterExternal` restores it, making the plugin lifecycle safe even if the same type is re-registered.

Sources: [src/lib/agents/adapters/registry.ts:140-185]()

### Provider-to-Adapter Maps

Two lookup tables in `registry.ts` drive adapter resolution:

```typescript
export const DEFAULT_ADAPTER_BY_PROVIDER_ID: Record<string, string> = {
  "claude-code":  "claude_local",
  "codex-cli":    "codex_local",
  "gemini-cli":   "gemini_local",
  "cursor-cli":   "cursor_local",
  "opencode":     "opencode_local",
  "pi":           "pi_local",
  "grok-cli":     "grok_local",
  "copilot-cli":  "copilot_local",
};

export const LEGACY_ADAPTER_BY_PROVIDER_ID: Record<string, string> = {
  "claude-code":  "claude_code_legacy",
  "codex-cli":    "codex_cli_legacy",
  // … one per provider
};
```

`defaultAdapterTypeForProvider(providerId?)` walks `DEFAULT_ADAPTER_BY_PROVIDER_ID` then falls back to the registry's `defaultAdapterType` (`"claude_local"`).

Sources: [src/lib/agents/adapters/registry.ts:25-46]()

---

## Built-in Adapters

All eight built-in adapters use `executionEngine: "structured_cli"` and delegate subprocess management to `runChildProcess` from `utils.ts`.

### Execution Engine Taxonomy

| Engine | Meaning | Adapters |
|---|---|---|
| `structured_cli` | Spawns CLI binary, parses structured output (JSON stream or plain text) | All `*_local` adapters |
| `legacy_pty_cli` | PTY terminal session; no `execute()` method; `experimental: true` | All `*_legacy` adapters |

### Adapter-by-Adapter Summary

| Adapter type | CLI command | Session resume | Stream format | Billing type |
|---|---|---|---|---|
| `claude_local` | `claude` | `--resume <sessionId>` (ID-based) | `--output-format stream-json` | `subscription` |
| `codex_local` | `codex` | `threadId` emitted on stdout | `--json` event stream | `unknown` |
| `gemini_local` | `gemini` | via session ID | JSON stream | `unknown` |
| `cursor_local` | `cursor` | `--resume <sessionId>` | `--output-format stream-json` | `subscription` |
| `opencode_local` | `opencode` | `--session <sessionId>` | `run --format json` | `unknown` |
| `pi_local` | `pi` | `--session <file>` (file-based) | `--mode json` | `unknown` |
| `grok_local` | `grok` | none | plain stdout | `api` |
| `copilot_local` | `copilot` | none | plain stdout | `subscription` |

Sources: [src/lib/agents/adapters/registry.ts:183-200](), [src/lib/agents/adapters/grok-local.ts:44-52](), [src/lib/agents/adapters/copilot-local.ts:44-52]()

### `claude_local` — Structured Streaming Adapter

The Claude adapter is the most feature-complete. Its `buildClaudeArgs` function assembles the CLI invocation:

```typescript
// src/lib/agents/adapters/claude-local.ts (condensed)
const args = [
  "-p", "--output-format", "stream-json",
  "--include-partial-messages", "--verbose",
  "--dangerously-skip-permissions",
];
if (resumeSessionId) args.push("--resume", resumeSessionId);
if (model)          args.push("--model", model);
if (effort)         args.push("--effort", effort);
if (systemPrompt)   args.push("--system-prompt", systemPrompt);
if (skillsDir) {
  args.push("--plugin-dir", skillsDir);
  args.push("--add-dir", skillsDir);
}
```

Skills are injected via `--plugin-dir` (to register them as slash-commands) and `--add-dir` (for read access). The session codec stores `{ resumeId: string }` and displays as `Claude · <8-char prefix>`.

Sources: [src/lib/agents/adapters/claude-local.ts:37-80]()

### `codex_local` — JSON Event Stream Adapter

Codex emits model-rejection errors as `{"type":"error",...}` events on stdout, not stderr. The adapter includes a provider-specific classifier that runs before the generic chain:

```typescript
// src/lib/agents/adapters/codex-local.ts
function classifyCodexModelUnavailable(stderr, exitCode) {
  // Matches: "not supported when using codex with a chatgpt account"
  // Returns kind: "model_unavailable" so the UI surfaces the right hint
}
```

The codex session codec uses a `threadId` field, and its args always include `--ephemeral --skip-git-repo-check --dangerously-bypass-approvals-and-sandbox`.

Sources: [src/lib/agents/adapters/codex-local.ts:26-68](), [src/lib/agents/adapters/codex-local.ts:74-103]()

### `pi_local` — File-Based Session Adapter

Pi's session state is a JSON file on disk rather than an ID string:

- Session files live in `~/.cabinet/pi-sessions/<runId>.json`.
- `ensureSessionFile(runId, stored)` creates the file on first use or reuses an existing path.
- The codec serializes/deserializes `{ sessionFile: string }` and uses the filename (minus extension) as the display ID.
- The model argument accepts `"provider/model"` notation, which `splitProviderModel` decomposes into separate `--provider` and `--model` flags.

Sources: [src/lib/agents/adapters/pi-local.ts:36-60](), [src/lib/agents/adapters/pi-local.ts:62-90]()

### `grok_local` / `copilot_local` — Plain Text Adapters

These two adapters do not have dedicated stream parsers; they treat the entire stdout as the assistant response. Both forward raw chunks directly to `ctx.onLog`, accumulate into `forwardedStdout`, and return the trimmed total as `output`. Neither supports session resume. Copilot passes `--allow-all-tools` to prevent headless runs from stalling on approval prompts.

Sources: [src/lib/agents/adapters/grok-local.ts:36-75](), [src/lib/agents/adapters/copilot-local.ts:33-68]()

---

## Per-Adapter Stream Parsers

Adapters with structured output use dedicated stream accumulator modules:

| Module | Adapter | Parser role |
|---|---|---|
| `claude-stream.ts` | `claude_local` | `stream-json` NDJSON → assistant text, session ID, model, usage, billing type |
| `codex-stream.ts` | `codex_local` | JSON event stream → display text, error events, thread ID, usage |
| `cursor-stream.ts` | `cursor_local` | `stream-json` NDJSON → text, session ID; detects unknown-session errors |
| `opencode-stream.ts` | `opencode_local` | JSON events → text, session ID, usage; detects unknown-session errors |
| `pi-stream.ts` | `pi_local` | JSON mode output → display text, errors, usage |
| `gemini-stream.ts` | `gemini_local` | Provider-specific streaming format |

Each module exposes a factory function (`create*Accumulator`), a streaming consumer (`consume*`), and a flush function to drain any buffered partial line at process exit.

---

## Error Classification

Every adapter implements `classifyError(stderr, exitCode)` using a two-layer composition:

```
classifyChain(stderr, exitCode, [
  providerSpecificClassifier?,   // e.g. classifyCodexModelUnavailable
  (s, c) => classifyCommonError(s, c, { providerDisplayName, cliCommand }),
])
```

`classifyCommonError` in `error-classification.ts` covers seven canonical `ConversationErrorKind` values:

| Kind | Trigger pattern |
|---|---|
| `cli_not_found` | `ENOENT`, `command not found`, `spawn ... ENOENT` |
| `auth_expired` | `not logged in`, `unauthorized`, `missing api key`, `401`, `403`, `token expired` |
| `rate_limited` | `rate-limit`, `429`, `too many requests`, `resource exhausted` |
| `session_expired` | `no conversation found`, `session not found`, `resume invalid` |
| `context_exceeded` | `context length exceeded`, `prompt too long`, `tokens exceed` |
| `transport` | `ECONNREFUSED`, `ECONNRESET`, `socket hang up`, `fetch failed` |
| `timeout` | `timed out`, `deadline exceeded` |

`classifyChain` stops at the first non-null result and falls back to `unknownClassification()`.

Sources: [src/lib/agents/adapters/error-classification.ts:20-100]()

---

## Session Codec

Adapters that support session resume attach an `AdapterSessionCodec`:

```typescript
interface AdapterSessionCodec {
  deserialize(raw: unknown): Record<string, unknown> | null;
  serialize(params: Record<string, unknown>): Record<string, unknown> | null;
  getDisplayId?(params: Record<string, unknown>): string | null;
}
```

The runner stores the serialized value alongside the run record and passes it back through `sessionParams` on the next turn. Before calling `execute()`, it calls `deserialize`, extracts `sessionId`, and passes it directly. The display ID is surfaced in the conversation header.

| Adapter | Session key | Example display |
|---|---|---|
| `claude_local` | `resumeId` | `Claude · a1b2c3d4` |
| `codex_local` | `threadId` | `Codex · a1b2c3d4` |
| `cursor_local` | `sessionId` | `Cursor · a1b2c3d4` |
| `opencode_local` | `sessionId` | provider-specific |
| `pi_local` | `sessionFile` | filename without extension |

Sources: [src/lib/agents/adapters/types.ts:76-84](), [src/lib/agents/adapters/claude-local.ts:14-32](), [src/lib/agents/adapters/pi-local.ts:63-80]()

---

## Plugin Loader

Third-party adapters are loaded from `~/.cabinet/adapter-plugins.json`:

```json
{
  "plugins": [
    { "package": "my-cabinet-adapter", "enabled": true, "type": "my_adapter" },
    { "path": "./local-adapter.js", "enabled": true }
  ]
}
```

`loadExternalAdapters()` in `plugin-loader.ts` is a promise-singleton (idempotent after the first call). For each enabled entry it:

1. Resolves the module path (absolute, relative to `cwd`, or bare package name).
2. Dynamic-`import()`s the module.
3. Calls `extractAdapter()`: tries `createAgentAdapter()`, then `createServerAdapter()`, then `module.default`, then `module.adapter` — whatever returns an object with a `type: string` field.
4. Optionally validates the `type` against the entry's declared `type` field.
5. Calls `agentAdapterRegistry.registerExternal(adapter)`.

`unloadExternalAdapters()` iterates `loadedPlugins` and calls `agentAdapterRegistry.unregisterExternal(type)` for each, restoring any displaced built-in.

Sources: [src/lib/agents/adapters/plugin-loader.ts:56-100](), [src/lib/agents/adapters/plugin-loader.ts:101-120]()

---

## Provider Registry

`ProviderRegistryImpl` (`provider-registry.ts`) is a separate singleton from the adapter registry. It holds `AgentProvider` objects — richer descriptions that include install instructions, binary candidates, `healthCheck()`, and `buildArgs`/`buildSessionInvocation`/`buildOneShotInvocation` contracts for PTY-based launch paths.

```typescript
export const providerRegistry = new ProviderRegistryImpl();
// default: "claude-code"

providerRegistry.register(claudeCodeProvider);
providerRegistry.register(codexCliProvider);
// … six more built-in providers
```

`listAvailable()` calls `provider.isAvailable()` on each entry concurrently and returns only those that pass. `providerSupportsEffort(providerId, effort)` is a utility used by the dispatcher to decide whether to forward a parent run's effort level to a child run when the provider differs.

Sources: [src/lib/agents/provider-registry.ts:12-55]()

---

## Per-Run Provider & Model Overrides at Launch Time

`provider-runtime.ts` is the bridge between stored run config and the actual subprocess. For **legacy PTY** runs, `buildLaunchSpec` resolves the provider, calls the appropriate `buildOneShotInvocation` or `buildSessionInvocation`, and returns a `ProviderLaunchSpec` (command + args). The override flow is:

```
getOneShotLaunchSpec({ providerId?, prompt, workdir, model?, effort?, resumeId? })
  → resolveProviderOrThrow(providerId)          // reads settings, resolves enabled provider
    → readProviderSettingsSync()
    → resolveEnabledProviderId(providerId, settings)
    → providerRegistry.get(resolvedId)
  → provider.buildOneShotInvocation(prompt, workdir, { model, effort })
  → resolveCliCommand(provider)                 // walks commandCandidates list
  → ProviderLaunchSpec { command, args, … }
```

For **structured** runs the runner bypasses `provider-runtime.ts` and calls `agentAdapterRegistry.get(adapterType)` directly, then passes `config.model` and `config.effort` through `AdapterExecutionContext.config`. The adapter reads them with `readStringConfig(config, "model")` and `readEffortConfig(config)` from `_shared/cli-args.ts` and appends the appropriate CLI flags.

Sources: [src/lib/agents/provider-runtime.ts:22-60](), [src/lib/agents/adapters/claude-local.ts:38-50](), [src/lib/agents/adapters/codex-local.ts:75-93]()

---

## Shared Subprocess Utilities

`utils.ts` provides the runtime infrastructure used by every structured adapter:

- **`ADAPTER_RUNTIME_PATH`** — A composed `PATH` string that includes `~/.local/bin`, `/usr/local/bin`, `/opt/homebrew/bin`, the active nvm bin, and the inherited `process.env.PATH`. Set as the `PATH` env var on every child process.
- **`withAdapterRuntimeEnv(env)`** — Merges values from `~/.cabinet.env` (mtime-cached) under the adapter runtime PATH. Caller-supplied env takes precedence over file values (dotenv convention).
- **`runChildProcess(command, args, options)`** — Spawns the child with `stdio: pipe`, wires `onStdout`/`onStderr` streaming callbacks, and manages timeout with a two-stage SIGTERM → SIGKILL (5 s grace period by default). On POSIX it kills the entire process group (`process.kill(-processGroupId, signal)`) to prevent orphaned subprocesses.
- **`resolveCommandFromCandidates(candidates)`** — Walks a list of path candidates, using `test -x` for absolute paths and `command -v` for bare names.

Sources: [src/lib/agents/adapters/utils.ts:10-50](), [src/lib/agents/adapters/utils.ts:97-155]()

---

## Summary

The adapter layer cleanly separates _what tool to use_ (provider registry) from _how to invoke it_ (adapter registry). Each of the eight built-in `*_local` adapters owns a dedicated arg-builder, an optional stream parser, a session codec, and a `classifyError` chain. Legacy `*_legacy` adapters share the same registry slots but carry `executionEngine: "legacy_pty_cli"` and expose no `execute()` method — the PTY runtime handles them directly. External adapters slot in via `~/.cabinet/adapter-plugins.json` using a module-agnostic export contract (`createAgentAdapter`, `default`, or `adapter`), with a built-in fallback preserved on displacement. Per-run model and effort overrides are threaded through `AdapterExecutionContext.config` for structured adapters and through `buildOneShotInvocation` opts for PTY adapters.
