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…
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.
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,
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: 0This 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.
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`.
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:
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.
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.
Two stores, two scopes:
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.
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 - The open-source visual workspace for Claude Code, Codex, and OpenCode. Run multiple coding agents in parallel, edit their work visually in markdown, mockups, and diagrams, and track tasks. Free, MIT-licensed desktop app for macOS, Windows, Linux, with mobile companion for iOS and Android.
Repo: nimbalyst/nimbalyst
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…
Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc captures everything learned so…