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,
$ npx -y skills add nimbalyst/nimbalyst --agent claude-codeHow 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.mdplanStatus:
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: 0Agentic 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
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: 0Agentic 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)`
Nimbalyst is a free, open-source, local, interactive visual editor & session/task manager for developers, product managers, designers, builders.
Repo: nimbalyst/nimbalyst
Other agents on nimbalyst.
- e2e-runner
Run E2E tests in a dev container for isolated, reproducible test execution. Use proactively when asked to run Playwright tests, E2E tests, or when in a worktree. Handles the full Docker container lifecycle automatically.
Open agent - codex-pre-edit-tracking-investigation
Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc captures everything learned so the next session can pick up cleanly without re-deriving.
Open agent

