agent-ecosystem/docs/research/best-abstraction-for-electron.md
iliya 71db7f153b feat(research): add comprehensive documentation on AI agent protocols and orchestration tools
- Introduced multiple new markdown files detailing the Agent Client Protocol (ACP), AI agent orchestration landscape, and various tools for managing multi-agent systems.
- Included in-depth analysis of protocol standards, governance structures, and emerging frameworks relevant to AI agent integration.
- Documented key features, architecture, and integration potential of various desktop and CLI orchestrators, enhancing understanding of the current ecosystem.
- Provided insights into best practices for integrating multi-provider agent support within the Electron framework.

This documentation aims to serve as a foundational resource for developers and stakeholders involved in AI agent orchestration and integration.
2026-03-25 14:47:52 +02:00

726 lines
28 KiB
Markdown

# Best Abstraction Tool for Multi-Provider Agent Support in Electron
**Date**: 2026-03-24
**Branch**: `dev`
**Based on**: actual source analysis of `TeamProvisioningService.ts` (7,982 LOC), `childProcess.ts`, `TeamMcpConfigBuilder.ts`, `PtyTerminalService.ts`, `agent-teams-controller/`, and prior research in `docs/research/`
---
## Context: What We Have Today
Our Electron app (40.x) manages Claude Code CLI processes via:
| Component | File | Role |
|-----------|------|------|
| `spawnCli()` | `src/main/utils/childProcess.ts` | child_process.spawn wrapper with Windows EINVAL fallback, injects `CLI_ENV_DEFAULTS` |
| `TeamProvisioningService` | `src/main/services/team/TeamProvisioningService.ts` | 7,982 LOC monolith: process lifecycle, stream-json NDJSON parsing, prompt engineering, stall watchdog, tool approval relay |
| `ClaudeBinaryResolver` | `src/main/services/team/ClaudeBinaryResolver.ts` | Resolves `claude` binary across PATH, NVM, platform dirs |
| `TeamMcpConfigBuilder` | `src/main/services/team/TeamMcpConfigBuilder.ts` | Builds `--mcp-config` JSON for every spawned process |
| `PtyTerminalService` | `src/main/services/infrastructure/PtyTerminalService.ts` | node-pty for embedded terminal (used separately, NOT for agent processes) |
| `agent-teams-controller` | `agent-teams-controller/` | Provider-agnostic file CRUD (tasks, kanban, inbox, reviews) |
| `killTeamProcess()` | TeamProvisioningService | Uses SIGKILL to prevent Claude CLI SIGTERM cleanup deleting team files |
**Current protocol**: Claude CLI `--input-format stream-json --output-format stream-json` — proprietary NDJSON with types: `user`, `assistant`, `control_request`, `result`, `system`.
**Current coupling**: 9/10 to Claude Code CLI (see `best-integration-approach.md` for full coupling map).
---
## Two Distinct Needs
### Level 1: CLI Agent Process Management
Spawn external CLI agents (Claude Code, Codex CLI, Gemini CLI, Goose) as child processes, each with its own protocol, binary resolution, health monitoring, and MCP config.
### Level 2: Programmatic LLM API Calls
Call LLM APIs directly for lightweight tasks (code review bot, triage bot, task planning, MCP tool calling). No CLI process — just HTTP to provider APIs.
These are **fundamentally different problems** and should use **different solutions**.
---
## Level 1: CLI Agent Process Management
### The Candidates
#### Option A: Own Adapter Pattern (Overstory-style)
**Reliability: 9/10 | Confidence: 9/10**
Build a thin `AgentCliAdapter` interface with per-CLI implementations.
```typescript
// src/main/services/agent/AgentCliAdapter.ts
export interface AgentCliAdapter {
readonly providerId: string; // 'claude' | 'codex' | 'gemini' | 'goose'
/** Resolve binary path on this machine */
resolveBinary(): Promise<string | null>;
/** Build spawn args for creating/launching a team */
buildSpawnArgs(request: AgentSpawnRequest): string[];
/** Build env vars for the spawned process */
buildEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
/** Parse a line of stdout. Returns typed event or null (skip). */
parseStdoutLine(line: string): AgentOutputEvent | null;
/** Format a user message for stdin */
formatUserMessage(text: string): string;
/** Process exit semantics: what does exit code mean? */
interpretExitCode(code: number | null): 'success' | 'error' | 'killed';
/** Kill semantics: SIGTERM vs SIGKILL */
killProcess(child: ChildProcess): void;
/** Whether this CLI needs MCP config file */
needsMcpConfig: boolean;
/** Build MCP config in the format this CLI expects */
buildMcpConfig?(servers: Record<string, McpServerConfig>): object;
}
```
Per-provider implementations:
```typescript
// src/main/services/agent/adapters/ClaudeCliAdapter.ts
export class ClaudeCliAdapter implements AgentCliAdapter {
readonly providerId = 'claude';
readonly needsMcpConfig = true;
async resolveBinary(): Promise<string | null> {
return new ClaudeBinaryResolver().resolve();
}
buildSpawnArgs(request: AgentSpawnRequest): string[] {
return [
'--input-format', 'stream-json',
'--output-format', 'stream-json',
'--verbose',
'--setting-sources', 'user,project,local',
'--mcp-config', request.mcpConfigPath!,
'--disallowedTools', 'TeamDelete,TodoWrite',
...(request.skipPermissions
? ['--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions']
: ['--permission-prompt-tool', 'stdio', '--permission-mode', 'default']),
...(request.model ? ['--model', request.model] : []),
];
}
buildEnv(base: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
return { ...base, CLAUDE_HOOK_JUDGE_MODE: 'true' };
}
parseStdoutLine(line: string): AgentOutputEvent | null {
const msg = JSON.parse(line);
// Existing 60+ branch logic from handleStreamJsonMessage()
switch (msg.type) {
case 'assistant': return { kind: 'text', content: extractText(msg) };
case 'result': return { kind: 'result', success: msg.subtype !== 'error' };
case 'control_request': return { kind: 'approval', request: msg };
// ... etc
}
}
formatUserMessage(text: string): string {
return JSON.stringify({
type: 'user',
message: { role: 'user', content: [{ type: 'text', text }] },
}) + '\n';
}
killProcess(child: ChildProcess): void {
killProcessTree(child, 'SIGKILL'); // SIGKILL to prevent cleanup
}
}
```
```typescript
// src/main/services/agent/adapters/CodexCliAdapter.ts
export class CodexCliAdapter implements AgentCliAdapter {
readonly providerId = 'codex';
readonly needsMcpConfig = false; // Codex uses MCP differently
async resolveBinary(): Promise<string | null> {
// which codex
return resolveWhich('codex');
}
buildSpawnArgs(request: AgentSpawnRequest): string[] {
return ['app-server']; // JSON-RPC mode
}
parseStdoutLine(line: string): AgentOutputEvent | null {
// JSON-RPC notification parsing
const msg = JSON.parse(line);
if (msg.method === 'item/agentMessage/delta') {
return { kind: 'text_delta', content: msg.params.delta };
}
// ...
}
formatUserMessage(text: string): string {
// JSON-RPC request for turn/start
return JSON.stringify({
jsonrpc: '2.0', id: nextId(),
method: 'turn/start',
params: { message: text },
}) + '\n';
}
killProcess(child: ChildProcess): void {
killProcessTree(child, 'SIGTERM'); // Codex handles SIGTERM gracefully
}
}
```
```typescript
// src/main/services/agent/adapters/GeminiCliAdapter.ts
export class GeminiCliAdapter implements AgentCliAdapter {
readonly providerId = 'gemini';
readonly needsMcpConfig = false;
async resolveBinary(): Promise<string | null> {
return resolveWhich('gemini');
}
buildSpawnArgs(request: AgentSpawnRequest): string[] {
return [
'--output-format', 'stream-json',
'-p', request.prompt,
];
}
parseStdoutLine(line: string): AgentOutputEvent | null {
// Gemini NDJSON events
const event = JSON.parse(line);
// ...
}
formatUserMessage(text: string): string {
// Gemini headless doesn't support multi-turn stdin in stream-json
// (one-shot with -p flag). For multi-turn, need new process per turn.
throw new Error('Gemini CLI does not support multi-turn stdin');
}
killProcess(child: ChildProcess): void {
killProcessTree(child, 'SIGTERM');
}
}
```
**Pros:**
- Zero new dependencies
- Perfectly fits existing `spawnCli()` / `killProcessTree()` infrastructure
- Each adapter is ~100-200 LOC — easy to test in isolation
- Can be extracted incrementally from the existing TeamProvisioningService
- No framework overhead in the Electron main process
- Each CLI's quirks handled explicitly (Claude SIGKILL vs Codex SIGTERM, stream-json vs JSON-RPC)
**Cons:**
- We write the adapter code ourselves (~500 LOC total for 4 adapters)
- No built-in CLI discovery / health check framework
**Effort**: ~800 LOC (interface + 4 adapters + factory), 3-5 days
---
#### Option B: node-pty Based Approach
**Reliability: 5/10 | Confidence: 4/10**
Use pseudo-terminal for all CLI agents (captures raw terminal output).
```typescript
import * as pty from 'node-pty';
const proc = pty.spawn('claude', ['--verbose'], {
name: 'xterm-256color',
cols: 120, rows: 40,
cwd: projectPath,
env: process.env,
});
proc.onData((data) => {
// Problem: raw terminal output with ANSI codes, cursor movement, etc.
// We'd need to strip all that to parse structured JSON
});
```
**Pros:**
- Already have `node-pty` in dependencies (for embedded terminal)
- Works with any CLI that has a TUI mode
**Cons:**
- node-pty is a native addon requiring electron-rebuild (fragile across platforms)
- All CLIs output ANSI escape codes in TTY mode — parsing structured data from raw terminal output is extremely unreliable
- We ALREADY use stream-json/JSON-RPC specifically to AVOID the TTY problem
- Memory overhead of full PTY per agent process
- Claude Code, Codex, and Gemini all have headless/programmatic modes — PTY is the WRONG abstraction
**Verdict: REJECT.** PTY is for interactive terminals, not programmatic agent management. We already learned this — `PtyTerminalService` is used only for the embedded terminal, not for agent processes.
---
#### Option C: MCO / Third-Party Orchestrator Library
**Reliability: 3/10 | Confidence: 3/10**
No mature, production-ready TypeScript library exists for "spawn and manage multiple AI CLI agents as child processes." The closest is `pi-builder` from the `awesome-cli-coding-agents` ecosystem, but it's a young project (~100 stars) with no stability guarantees.
**Verdict: REJECT.** The problem is too niche and CLI-specific for a generic library. Each CLI has its own protocol (Claude stream-json, Codex JSON-RPC, Gemini NDJSON, Goose recipes). A generic library would either be too thin to be useful or too opinionated to handle the differences.
---
#### Level 1 Recommendation: Option A (Own Adapter Pattern)
| Criteria | Score |
|----------|-------|
| Fit with existing code patterns | 10/10 — mirrors how `spawnCli()` and `ClaudeBinaryResolver` already work |
| Lines of code to integrate | ~800 LOC (interface + 4 adapters + factory) |
| Heavy dependencies added | 0 |
| Runs in Electron main process | Yes (pure Node.js) |
| License compatibility | N/A (our own code, AGPL-3.0) |
| Active maintenance | By us — full control |
**Migration path**: Extract current Claude-specific logic from `TeamProvisioningService` into `ClaudeCliAdapter`, then add adapters for other CLIs one by one. The monster 7,982 LOC monolith gets decomposed as a side effect.
---
## Level 2: Programmatic LLM API Calls
### The Candidates
#### Option A: Vercel AI SDK (`ai` + `@ai-sdk/*`)
**Reliability: 9/10 | Confidence: 9/10** (Recommended)
The leading TypeScript LLM abstraction. 20M+ monthly npm downloads, backed by Vercel, 30K+ GitHub stars.
```typescript
// src/main/services/llm/LlmService.ts
import { generateText, streamText, tool } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { google } from '@ai-sdk/google';
import { z } from 'zod';
// Simple code review — runs in Electron main process
export async function reviewCode(diff: string, model = 'anthropic/claude-sonnet-4-20250514') {
const { text } = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
system: 'You are a code reviewer. Be concise.',
prompt: `Review this diff:\n\n${diff}`,
});
return text;
}
// Streaming task planning with tool calling — relayed to renderer via IPC
export async function planTasks(
description: string,
onChunk: (text: string) => void,
) {
const result = streamText({
model: openai('gpt-4o'),
system: 'You are a project planner.',
prompt: description,
tools: {
createTask: tool({
description: 'Create a new task on the kanban board',
parameters: z.object({
title: z.string(),
assignee: z.string().optional(),
column: z.enum(['backlog', 'todo', 'in_progress']),
}),
execute: async ({ title, assignee, column }) => {
// Call our agent-teams-controller to create task
return controller.createTask({ title, assignee, column });
},
}),
},
maxSteps: 10, // Allow multi-step tool calling loops
});
for await (const chunk of result.textStream) {
onChunk(chunk);
}
}
// Triage incoming issue — pick best team member
export async function triageTask(taskDescription: string) {
const { object } = await generateObject({
model: google('gemini-2.5-flash'),
schema: z.object({
assignee: z.string(),
priority: z.enum(['low', 'medium', 'high', 'critical']),
reasoning: z.string(),
}),
prompt: `Triage this task: ${taskDescription}\nAvailable members: alice (frontend), bob (backend), carol (devops)`,
});
return object; // Typed: { assignee: string; priority: string; reasoning: string }
}
```
**What we install:**
```bash
pnpm add ai @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/google zod
# ai: 67.5 kB gzipped (core)
# @ai-sdk/anthropic: ~15 kB gzipped
# @ai-sdk/openai: ~19.5 kB gzipped
# @ai-sdk/google: ~15 kB gzipped
# Total: ~117 kB gzipped — very reasonable for Electron
```
**Pros:**
- Unified `generateText()` / `streamText()` / `generateObject()` API across ALL providers
- Swap provider with one line change: `anthropic('claude-sonnet-4-20250514')``openai('gpt-4o')`
- First-class tool calling with Zod schema validation
- Streaming works perfectly in Node.js (Electron main process)
- Sentry already has `vercelAIIntegration` for Electron — we already use `@sentry/electron`
- TypeScript-first: full type inference for tool parameters and structured outputs
- AI SDK 6 `Agent` class for reusable agent patterns
- 20M+ monthly downloads, extremely active maintenance, battle-tested
- Apache-2.0 license — compatible with our AGPL-3.0
**Cons:**
- Adds ~4 new deps (ai, 3 providers) — but they're lightweight
- Learning curve for Zod schemas (though Zod is industry standard)
- AI SDK 5→6 had some breaking changes — minor version churn risk
**Electron main process integration:**
```typescript
// src/main/ipc/llm.ts — IPC handlers for renderer
import { wrapHandler } from './utils';
import { streamText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
export function registerLlmHandlers() {
// One-shot generation
ipcMain.handle('llm:generate', wrapHandler(async (_event, params) => {
const { text } = await generateText({
model: resolveModel(params.model), // 'anthropic/claude-sonnet-4-20250514' → anthropic('claude-sonnet-4-20250514')
system: params.system,
prompt: params.prompt,
});
return { text };
}));
// Streaming — emit chunks via webContents.send()
ipcMain.handle('llm:stream', wrapHandler(async (event, params) => {
const result = streamText({
model: resolveModel(params.model),
system: params.system,
prompt: params.prompt,
});
const sender = event.sender;
for await (const chunk of result.textStream) {
sender.send('llm:chunk', { requestId: params.requestId, chunk });
}
sender.send('llm:done', { requestId: params.requestId });
return { started: true };
}));
}
```
---
#### Option B: Mastra (LLM layer only)
**Reliability: 6/10 | Confidence: 5/10**
Mastra is a full agent framework (workflows, RAG, memory, server). Using "just the LLM layer" means using Mastra's `Agent` class which internally uses AI SDK anyway.
```typescript
import { Agent } from '@mastra/core/agent';
const reviewer = new Agent({
id: 'code-reviewer',
instructions: 'You are a code reviewer.',
model: 'anthropic/claude-sonnet-4-20250514',
});
const result = await reviewer.generate('Review this diff...');
```
**Pros:**
- Nice `Agent` abstraction with built-in memory and workflow support
- Uses AI SDK internally — same providers
- TypeScript-native
**Cons:**
- `@mastra/core` pulls in significant dependencies (server framework, storage adapters, DI container)
- Overkill for our use case — we need `generateText()`, not the full agent runtime
- Our agent runtime IS the CLI process management layer, not Mastra's in-process loop
- Less mature than AI SDK (smaller community, fewer downloads)
- Adds unnecessary abstraction layer on top of AI SDK
- YC-backed startup — could pivot or die; AI SDK is backed by Vercel ($3.2B company)
**See also:** `docs/research/mastra-integration-analysis.md` (full analysis, verdict 6/10 feasibility)
---
#### Option C: LangChain.js
**Reliability: 4/10 | Confidence: 3/10**
```typescript
import { ChatAnthropic } from '@langchain/anthropic';
import { ChatOpenAI } from '@langchain/openai';
const chat = new ChatAnthropic({ model: 'claude-sonnet-4-20250514' });
const result = await chat.invoke('Review this diff...');
```
**Pros:**
- Largest ecosystem (chains, agents, RAG, memory)
- Many tutorials and examples
**Cons:**
- **101 kB gzipped** — 3x the size of OpenAI SDK, 1.5x AI SDK
- Heavy dependency tree (infamous for bloat)
- Frequent breaking changes between versions
- Overcomplicated abstractions for simple LLM calls
- Edge runtime incompatible (uses Node `fs`)
- Community frustration well-documented: "LangChain adds unnecessary complexity"
- For our use case (simple API calls with tool calling), it's a 10-ton truck for a bicycle ride
---
#### Option D: LiteLLM (via proxy)
**Reliability: 5/10 | Confidence: 4/10**
Run a Python proxy process, point OpenAI SDK at it.
```typescript
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'http://localhost:4000', // LiteLLM proxy
apiKey: 'sk-anything',
});
const result = await client.chat.completions.create({
model: 'anthropic/claude-sonnet-4-20250514',
messages: [{ role: 'user', content: 'Review this diff...' }],
});
```
**Pros:**
- 100+ providers through OpenAI-compatible API
- Rate limiting, fallbacks, cost tracking built-in
- Established in production at many companies
**Cons:**
- **Requires Python runtime** — catastrophic for an Electron desktop app
- Another long-lived process to manage (proxy lifecycle)
- Performance degrades under concurrency (Python GIL)
- Extra latency hop: Electron → proxy → provider → proxy → Electron
- Enterprise features (SSO, RBAC) behind paid license
- Electron users expect a self-contained app, not "also install Python 3.11"
---
#### Option E: Direct Provider SDKs with Thin Wrapper
**Reliability: 7/10 | Confidence: 7/10**
```typescript
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
async function callLlm(provider: string, prompt: string) {
switch (provider) {
case 'anthropic': {
const client = new Anthropic();
const msg = await client.messages.create({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [{ role: 'user', content: prompt }],
});
return msg.content[0].type === 'text' ? msg.content[0].text : '';
}
case 'openai': {
const client = new OpenAI();
const result = await client.chat.completions.create({
model: 'gpt-4o',
messages: [{ role: 'user', content: prompt }],
});
return result.choices[0]?.message?.content ?? '';
}
// ...each provider has different API shape
}
}
```
**Pros:**
- Each SDK is lightweight and well-maintained
- No abstraction overhead — direct control
**Cons:**
- Must implement unified tool calling ourselves (Anthropic tools format ≠ OpenAI function calling ≠ Google tool format)
- Must implement streaming ourselves for each provider
- Must implement structured output extraction per-provider
- Maintenance burden grows linearly with each new provider
- This is literally what AI SDK already does, but worse
---
### Level 2 Recommendation: Option A (Vercel AI SDK)
| Criteria | Score |
|----------|-------|
| Fit with existing code patterns | 9/10 — pure TypeScript, Node.js-compatible, modular |
| Lines of code to integrate | ~200 LOC (LlmService + IPC handlers) |
| Heavy dependencies added | No — ~117 kB gzipped total for core + 3 providers |
| Runs in Electron main process | Yes — confirmed by Sentry Electron integration docs |
| License compatibility | Apache-2.0 → compatible with our AGPL-3.0 |
| Active maintenance | 10/10 — 20M+ monthly downloads, Vercel-backed |
---
## Combined Architecture
```
┌─────────────────────────────────────────────────────────┐
│ Electron Main Process │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Level 1: CLI Process Management │ │
│ │ │ │
│ │ AgentCliAdapter (interface) │ │
│ │ ├─ ClaudeCliAdapter (stream-json NDJSON) │ │
│ │ ├─ CodexCliAdapter (app-server JSON-RPC) │ │
│ │ ├─ GeminiCliAdapter (stream-json NDJSON) │ │
│ │ └─ GooseCliAdapter (stdin recipes) │ │
│ │ │ │
│ │ spawnCli() + killProcessTree() (unchanged) │ │
│ │ TeamMcpConfigBuilder (unchanged) │ │
│ │ TeamProvisioningService (refactored to use │ │
│ │ adapter.parseStdoutLine() etc.) │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Level 2: Programmatic LLM API Calls │ │
│ │ │ │
│ │ Vercel AI SDK (ai + @ai-sdk/*) │ │
│ │ ├─ generateText() → code review, triage │ │
│ │ ├─ streamText() → task planning, chat │ │
│ │ ├─ generateObject()→ structured extraction │ │
│ │ └─ tool() → MCP tool bridges │ │
│ │ │ │
│ │ LlmService.ts (~200 LOC) │ │
│ │ IPC handlers → renderer │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Shared: agent-teams-controller │ │
│ │ (provider-agnostic task/kanban/inbox CRUD) │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## Comparison Matrix
### Level 1: CLI Process Management
| Criterion | Own Adapter | node-pty | MCO/Third-Party |
|-----------|-------------|----------|-----------------|
| Reliability | 9/10 | 5/10 | 3/10 |
| Confidence | 9/10 | 4/10 | 3/10 |
| Fit with codebase | 10/10 | 4/10 | 3/10 |
| New dependencies | 0 | 0 (already have) | Unknown |
| LOC to integrate | ~800 | ~600 | ~1000+ |
| Electron compatible | Yes | Yes (fragile) | Unknown |
| Handles protocol diffs | Explicit | No (raw PTY) | Generic/lossy |
### Level 2: Programmatic LLM API Calls
| Criterion | AI SDK | Mastra | LangChain | LiteLLM | Direct SDKs |
|-----------|--------|--------|-----------|---------|-------------|
| Reliability | 9/10 | 6/10 | 4/10 | 5/10 | 7/10 |
| Confidence | 9/10 | 5/10 | 3/10 | 4/10 | 7/10 |
| Fit with codebase | 9/10 | 5/10 | 3/10 | 2/10 | 7/10 |
| Bundle size | 117 kB | ~400+ kB | 101 kB + deps | N/A (Python) | ~80 kB |
| Tool calling | Unified | Unified (via AI SDK) | Unified | OpenAI-compat | Per-provider |
| Streaming | Async iterator | Async iterator | Chains | SSE proxy | Per-provider |
| Providers | 20+ | 94 (via AI SDK) | 20+ | 100+ | Each separate |
| Electron main proc | Confirmed | Untested | Problematic | Requires Python | Yes |
| License | Apache-2.0 | Elastic-2.0 / AGPL-3.0 | MIT | MIT | Varies |
| Maintenance | Vercel (huge team) | Startup (small) | Community | Community | Per-vendor |
---
## Final Recommendation
### Level 1: Own Adapter Pattern
- **0 new dependencies**, ~800 LOC
- Extract Claude-specific logic from the 7,982 LOC monolith into `ClaudeCliAdapter`
- Add `CodexCliAdapter`, `GeminiCliAdapter`, `GooseCliAdapter` incrementally
- Each adapter handles that CLI's unique protocol, binary resolution, spawn args, kill semantics
- Decomposes the monolith as a beneficial side effect
### Level 2: Vercel AI SDK (`ai` + `@ai-sdk/*`)
- **4 lightweight deps** (~117 kB gzipped total), ~200 LOC integration
- `generateText()` for one-shot tasks, `streamText()` for interactive, `generateObject()` for structured extraction
- Unified tool calling with Zod schemas
- Swap any provider with one line change
- Apache-2.0 compatible with our AGPL-3.0
- Already used by 20M+ monthly projects, confirmed Electron compatibility
### Implementation Order
1. **Week 1**: Create `AgentCliAdapter` interface, extract `ClaudeCliAdapter` from `TeamProvisioningService`
2. **Week 1**: Install AI SDK, create `LlmService.ts` with `generateText()` wrapper, add IPC handlers
3. **Week 2**: Add `CodexCliAdapter` (app-server JSON-RPC mode)
4. **Week 2**: Build code review bot using AI SDK + MCP tools
5. **Week 3**: Add `GeminiCliAdapter`, `GooseCliAdapter`
6. **Week 3**: Build triage bot, task planning with `streamText()` + tool calling
**Total effort**: ~3 weeks for full multi-provider support at both levels.
---
## Sources
### AI SDK (Vercel)
- [AI SDK Introduction](https://ai-sdk.dev/docs/introduction)
- [AI SDK 6 Announcement](https://vercel.com/blog/ai-sdk-6)
- [Node.js Getting Started](https://ai-sdk.dev/docs/getting-started/nodejs)
- [Providers and Models](https://ai-sdk.dev/docs/foundations/providers-and-models)
- [Sentry Electron + Vercel AI Integration](https://docs.sentry.io/platforms/javascript/guides/electron/configuration/integrations/vercelai/)
- [Generating Text](https://ai-sdk.dev/docs/ai-sdk-core/generating-text)
- [npm: ai](https://www.npmjs.com/package/ai)
- [GitHub: vercel/ai](https://github.com/vercel/ai)
### Codex CLI
- [Codex SDK](https://developers.openai.com/codex/sdk)
- [Codex App Server](https://developers.openai.com/codex/app-server)
- [npm: @openai/codex-sdk](https://www.npmjs.com/package/@openai/codex-sdk)
- [CLI Reference](https://developers.openai.com/codex/cli/reference)
### Gemini CLI
- [Headless Mode Reference](https://geminicli.com/docs/cli/headless/)
- [GitHub: google-gemini/gemini-cli](https://github.com/google-gemini/gemini-cli)
### Goose
- [GitHub: block/goose](https://github.com/block/goose)
- [CLI Commands](https://block.github.io/goose/docs/guides/goose-cli-commands/)
### Mastra
- [GitHub: mastra-ai/mastra](https://github.com/mastra-ai/mastra)
- [Mastra Docs: Models](https://mastra.ai/models)
### LangChain.js
- [LangChain vs Vercel AI SDK vs OpenAI SDK: 2026 Guide](https://strapi.io/blog/langchain-vs-vercel-ai-sdk-vs-openai-sdk-comparison-guide)
- [Bundle Size Issue #809](https://github.com/langchain-ai/langchainjs/issues/809)
- [LangChain Criticism](https://community.latenode.com/t/why-im-avoiding-langchain-in-2025/39046)
### LiteLLM
- [LiteLLM Proxy Docs](https://docs.litellm.ai/docs/simple_proxy)
- [Best LiteLLM Alternatives 2026](https://www.getmaxim.ai/articles/best-litellm-alternatives-in-2026/)
### License Compatibility
- [Apache License and GPL Compatibility](https://www.apache.org/licenses/GPL-compatibility.html)
- [Apache 2.0 Compatible Licenses Guide](https://licensecheck.io/guides/apache-compatible)
### Ecosystem
- [CLI Coding Agents Comparison 2026](https://www.tembo.io/blog/coding-cli-tools-comparison)
- [awesome-cli-coding-agents](https://github.com/bradAGI/awesome-cli-coding-agents)