- 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.
28 KiB
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.
// 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:
// 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
}
}
// 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
}
}
// 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).
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-ptyin 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.
// 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:
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
vercelAIIntegrationfor Electron — we already use@sentry/electron - TypeScript-first: full type inference for tool parameters and structured outputs
- AI SDK 6
Agentclass 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:
// 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.
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
Agentabstraction with built-in memory and workflow support - Uses AI SDK internally — same providers
- TypeScript-native
Cons:
@mastra/corepulls 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
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.
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
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,GooseCliAdapterincrementally - 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
- Week 1: Create
AgentCliAdapterinterface, extractClaudeCliAdapterfromTeamProvisioningService - Week 1: Install AI SDK, create
LlmService.tswithgenerateText()wrapper, add IPC handlers - Week 2: Add
CodexCliAdapter(app-server JSON-RPC mode) - Week 2: Build code review bot using AI SDK + MCP tools
- Week 3: Add
GeminiCliAdapter,GooseCliAdapter - 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
- AI SDK 6 Announcement
- Node.js Getting Started
- Providers and Models
- Sentry Electron + Vercel AI Integration
- Generating Text
- npm: ai
- GitHub: vercel/ai