e2e-runner
AgentRun E2E tests in a dev container for isolated, reproducible test execution. Use proactively when asked to run Playwright tests,…
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,
Claude Code
> /plugin marketplace add nimbalyst/nimbalyst> /plugin install nimbalyst-wiki@nimbalyst
Cursor
Cursor also loads subagents from .claude/agents, so copy this file there to use it in Cursor.
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.
Run E2E tests in a dev container for isolated, reproducible test execution. Use proactively when asked to run Playwright tests,…
Status: **STUCK**. Three approaches tried, none reliably solves the pre-edit race for `update`-kind file_change items. This doc…
Google cross-vendor agent named for the model it runs — Gemini 4 Argon, Google’s top reasoning model, via…
Fresh-context, read-only verifier for a proposed claude-obsidian change or release. Inspects the requested staged diff, unstaged…
Inspect or update Inbox Zero rules and analytics through the public API CLI. Use when tasks involve rules, stats, or API-driven…