Skip to content
Productivity
Agent

agent-provider-architecture

This document is a reference for implementing a new **agent provider** in Nimbalyst. It is the architectural counterpart to `docs/AI_PROVIDER_TYPES.md` (which is end-user / product oriented) and walks through every seam a new agent has to fit through: session start and resume,

From plugin
nimbalyst
1.4k3 skills3 agents35 commands
Install
$ npx -y skills add nimbalyst/nimbalyst --agent claude-code

How it fires

How this agent gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.

Context preview

The summary Claude sees to decide when to auto-load this agent.

This document is a reference for implementing a new **agent provider** in Nimbalyst. It is the architectural counterpart to `docs/AI_PROVIDER_TYPES.md` (which is end-user / product oriented) and walks through every seam a new agent has to fit through: session start and resume,

Agent definition

agent-provider-architecture.md
planStatus:
  planId: plan-agent-provider-architecture
  title: Agentic Provider Architecture
  status: draft
  planType: system-design
  priority: medium
  owner: ghinkle
  stakeholders:
    - ghinkle
  tags:
    - ai
    - agents
    - architecture
    - reference
  created: "2026-04-25"
  updated: "2026-04-25T00:00:00.000Z"
  progress: 0

Agentic Provider Architecture

This document is a reference for implementing a new **agent provider** in Nimbalyst. It is the architectural counterpart to `docs/AI_PROVIDER_TYPES.md` (which is end-user / product oriented) and walks through every seam a new agent has to fit through: session start and resume, prompt handling, transcript output, tool calling, MCP configuration, and file-edit tracking.

A companion document, [`agent-providers-as-extensions.md`](./agent-providers-as-extensions.md), explores how the same surfaces could one day be exposed to extensions.

1. Two-layer abstraction

Nimbalyst splits an agent into **two stacked interfaces**, not one. New providers fill in both.

| Layer | File | Lifetime | Purpose | | --- | --- | --- | --- | | `AIProvider` | `packages/runtime/src/ai/server/AIProvider.ts:47` | One per `(provider type, sessionId)`, cached in `ProviderFactory` | High-level provider, owns config/system prompt/auth, drives the session, writes raw messages, emits stream chunks | | `AgentProtocol` | `packages/runtime/src/ai/server/protocols/ProtocolInterface.ts:187` | One per provider instance (or shared singleton) | Transport adapter — speaks the SDK / wire protocol of the underlying agent and yields normalized `ProtocolEvent`s |

The split exists so that a provider can stay stable across SDK upgrades and so that protocol adapters are unit-testable without dragging in DB, IPC, or auth. **Chat providers** (`ClaudeProvider`, `OpenAIProvider`, `LMStudioProvider`) skip the protocol layer entirely — they call vendor APIs directly inside `sendMessage`. **Agent providers** (`ClaudeCodeProvider`, `OpenAICodexProvider`, `CopilotCLIProvider`, `OpenCodeProvider`) implement `AIProvider` and delegate transport to a corresponding `AgentProtocol`.

2. The `AgentProtocol` contract

Verbatim from `ProtocolInterface.ts`:

interface AgentProtocol {
  readonly platform: string;
  createSession(options: SessionOptions): Promise<ProtocolSession>;
  resumeSession(sessionId: string, options: SessionOptions): Promise<ProtocolSession>;
  forkSession(sessionId: string, options: SessionOptions): Promise<ProtocolSession>;
  sendMessage(session: ProtocolSession, message: ProtocolMessage): AsyncIterable<ProtocolEvent>;
  abortSession(session: ProtocolSession): void;
  cleanupSession(session: ProtocolSession): void;
}

Inputs:

  • `SessionOptions`: `workspacePath`, `model`, `systemPrompt`, `abortSignal`, `permissionMode`, `mcpServers`, `env`, `allowedTools`, `disallowedTools`, plus a `raw` escape hatch for platform-specific knobs.
  • `ProtocolMessage`: `content` (text), `attachments` (image / pdf), `sessionId` (for logging), `mode` (`'planning' | 'agent'`).

Output is a unified `ProtocolEvent` stream. Event types:

type ProtocolEventType =
  | 'raw_event' | 'text' | 'reasoning'
  | 'tool_call' | 'tool_result'
  | 'error' | 'complete' | 'usage'
  | 'planning_mode_entered' | 'planning_mode_exited';

A new agent's only job at this layer is: **translate its native event stream into this normalized event vocabulary**, and capture the platform-native session ID into `session.id` as soon as the underlying SDK reveals it.

2.1 The four reference adapters

The four shipping protocols deliberately use different transports — together they exercise every transport family we've considered.

| Adapter | File | Transport | Native session unit | Forking | | --- | --- | --- | --- | --- | | `ClaudeSDKProtocol` | `protocols/ClaudeSDKProtocol.ts:48` | In-process function call to `query()` from `@anthropic-ai/claude-agent-sdk` | Session is implicit — created by first `query()`, ID arrives in stream | Native (`forkSession: true`) | | `CodexSDKProtocol` | `protocols/CodexSDKProtocol.ts:44` | In-process SDK from `@openai/codex-sdk` that spawns a native binary subprocess (`asarUnpack`'d in packaged builds) | Thread, `client.startThread()` | Not supported — degrades to new thread | | `CopilotACPProtocol` | `protocols/CopilotACPProtocol.ts:59` | Long-lived subprocess `copilot --acp --stdio` over JSON-RPC framed by readline | ACP `session/new` returns ID | Not supported | | `OpenCodeSDKProtocol` | `protocols/OpenCodeSDKProtocol.ts:44` | Reference-counted subprocess server, communicated with via HTTP + Server-Sent Events | Server-managed `session.create` | Not supported |

The shape is consistent regardless of transport: the adapter wraps process / socket / function-call lifecycle and yields `ProtocolEvent`s.

3. Session lifecycle

3.1 Where session state lives

Two stores, two scopes:

  • **Database (`ai_sessions` table)** — durable. One row per Nimbalyst session. Holds `provider`, `model`, `provider_session_id` (the platform-native ID returned by the protocol), `workspace_id`, `parent_session_id`, `worktree_id`, `mode`, and JSONB `metadata`. See `AISessionsRepository` (`packages/runtime/src/storage/repositories/AISessionsRepository.ts`).
  • **In-memory caches** — non-durable. `ProviderFactory.providers` keyed by `${type}-${sessionId}` (`ProviderFactory.ts:15`). Each protocol may also hold transport state (active subprocess, active queries).

The Nimbalyst `sessionId` is the canonical ID the rest of the system uses (UI, transcript, file tracking). The `provider_session_id` is platform-specific and belongs to the protocol — never assume the formats are interchangeable.

3.2 Starting a new session

1. UI / IPC asks for a session: `SessionHandlers.ts` calls `SessionManager.createSession({...})`. 2. `SessionManager` writes a row to `ai_sessions` and returns a Nimbalyst `sessionId`. 3. On first prompt: `ProviderFactory.createProvider(type, sessionId)`

Read more
Ships withnimbalyst

Nimbalyst is a free, open-source, local, interactive visual editor & session/task manager for developers, product managers, designers, builders.

Get the whole plugin