Skip to main content

Overview

While the simple examples demonstrate the core concepts, production implementations showcase how ACP is used in real-world applications with additional patterns for error handling, state management, and integration with AI models.

Gemini CLI Agent

Google Gemini CLI

A complete, production-ready ACP agent implementation using Google’s Gemini AI models
The Gemini CLI Agent is an excellent reference for building production-grade agents. It demonstrates:

LLM Integration

Complete integration with Google’s Gemini API including streaming responses and function calling

Robust Error Handling

Comprehensive error handling, retry logic, and graceful degradation

Advanced Tool Management

Dynamic tool registration, execution, and result handling

Production Patterns

Real-world patterns for state management, logging, and debugging

Production Architecture Patterns

Agent Architecture

1

Modular Design

Separate concerns into distinct components:
class ProductionAgent {
  private llmClient: LLMClient;           // AI model integration
  private toolRegistry: ToolRegistry;      // Available tools
  private sessionManager: SessionManager;  // Session state
  private permissionHandler: PermissionHandler; // Permission logic
  
  constructor(connection: AgentSideConnection) {
    // Initialize components
  }
}
2

State Management

Maintain comprehensive session state:
interface ProductionSession {
  id: string;
  conversationHistory: Message[];
  pendingToolCalls: Map<string, ToolCall>;
  activeAbortController: AbortController | null;
  context: {
    cwd: string;
    environment: Record<string, string>;
    capabilities: ClientCapabilities;
  };
}
3

Error Boundaries

Implement robust error handling at each layer:
async prompt(params: PromptRequest): Promise<PromptResponse> {
  try {
    return await this.executePrompt(params);
  } catch (error) {
    if (error instanceof CancellationError) {
      return { stopReason: "cancelled" };
    }
    if (error instanceof PermissionDeniedError) {
      return { stopReason: "permission_denied" };
    }
    // Log and report unexpected errors
    this.logger.error("Prompt execution failed", error);
    throw error;
  }
}
4

Streaming Integration

Stream LLM responses in real-time:
for await (const chunk of llmClient.stream(prompt)) {
  if (abortSignal.aborted) break;
  
  await this.connection.sessionUpdate({
    sessionId,
    update: {
      sessionUpdate: "agent_message_chunk",
      content: { type: "text", text: chunk.text },
    },
  });
}

Client Architecture

1

Agent Lifecycle Management

Manage agent processes robustly:
class AgentManager {
  private agentProcess: ChildProcess | null = null;
  private connection: ClientSideConnection | null = null;
  
  async start(): Promise<void> {
    // Spawn agent with proper error handling
    this.agentProcess = spawn(agentCommand, agentArgs, {
      stdio: ['pipe', 'pipe', 'pipe'],
    });
    
    // Handle process events
    this.agentProcess.on('error', this.handleProcessError);
    this.agentProcess.on('exit', this.handleProcessExit);
    
    // Establish connection
    await this.initializeConnection();
  }
  
  async stop(): Promise<void> {
    // Graceful shutdown
    await this.connection?.close();
    this.agentProcess?.kill('SIGTERM');
  }
}
2

Permission Policies

Implement sophisticated permission handling:
class PermissionPolicy {
  async evaluate(
    toolCall: ToolCall,
  ): Promise<'auto_approve' | 'request_user' | 'auto_deny'> {
    // Auto-approve safe operations
    if (toolCall.kind === 'read' && this.isInWorkspace(toolCall.path)) {
      return 'auto_approve';
    }
    
    // Auto-deny dangerous operations
    if (this.isDangerousPath(toolCall.path)) {
      return 'auto_deny';
    }
    
    // Ask user for everything else
    return 'request_user';
  }
}
3

UI Integration

Connect ACP to your application’s UI:
class UIBridge {
  async sessionUpdate(update: SessionUpdate): Promise<void> {
    switch (update.sessionUpdate) {
      case 'agent_message_chunk':
        this.chatView.appendMessage(update.content.text);
        break;
      case 'tool_call':
        this.toolPanel.showToolExecution(update);
        break;
      case 'agent_thought_chunk':
        this.reasoningPanel.appendThought(update.content.text);
        break;
    }
  }
}

Best Practices from Production

Logging and Observability

import { Logger } from 'winston';

class ObservableAgent implements Agent {
  private logger: Logger;
  
  async prompt(params: PromptRequest): Promise<PromptResponse> {
    this.logger.info('Prompt received', {
      sessionId: params.sessionId,
      promptLength: params.prompt.length,
    });
    
    const startTime = Date.now();
    try {
      const result = await this.executePrompt(params);
      
      this.logger.info('Prompt completed', {
        sessionId: params.sessionId,
        duration: Date.now() - startTime,
        stopReason: result.stopReason,
      });
      
      return result;
    } catch (error) {
      this.logger.error('Prompt failed', {
        sessionId: params.sessionId,
        duration: Date.now() - startTime,
        error: error.message,
      });
      throw error;
    }
  }
}

Performance Optimization

class BatchedSessionUpdates {
  private pending: SessionUpdate[] = [];
  private flushTimer: NodeJS.Timeout | null = null;
  
  queue(update: SessionUpdate): void {
    this.pending.push(update);
    
    if (!this.flushTimer) {
      this.flushTimer = setTimeout(() => this.flush(), 16); // ~60fps
    }
  }
  
  private async flush(): Promise<void> {
    const updates = this.pending.splice(0);
    this.flushTimer = null;
    
    // Send batched updates
    for (const update of updates) {
      await this.connection.sessionUpdate(update);
    }
  }
}

Security Considerations

Production implementations must carefully consider security:
  • Path validation: Always validate and sanitize file paths
  • Permission boundaries: Enforce strict permission policies
  • Resource limits: Limit agent resource consumption (CPU, memory, file I/O)
  • Audit logging: Log all sensitive operations
  • Input sanitization: Validate all user inputs before passing to agents
Path Security
class SecurePathValidator {
  constructor(private workspaceRoot: string) {}
  
  validate(requestedPath: string): string {
    // Resolve to absolute path
    const resolved = path.resolve(this.workspaceRoot, requestedPath);
    
    // Ensure it's within workspace
    if (!resolved.startsWith(this.workspaceRoot)) {
      throw new SecurityError('Path outside workspace');
    }
    
    // Check for sensitive directories
    const sensitive = ['.git', '.env', 'node_modules/.env'];
    if (sensitive.some(dir => resolved.includes(dir))) {
      throw new SecurityError('Access to sensitive path denied');
    }
    
    return resolved;
  }
}

Testing Strategies

class MockClient implements Client {
  public receivedUpdates: SessionUpdate[] = [];
  public permissionResponses: Map<string, PermissionOutcome> = new Map();
  
  async sessionUpdate(params: SessionNotification): Promise<void> {
    this.receivedUpdates.push(params.update);
  }
  
  async requestPermission(
    params: RequestPermissionRequest
  ): Promise<RequestPermissionResponse> {
    const outcome = this.permissionResponses.get(params.toolCall.toolCallId);
    if (!outcome) {
      throw new Error('Unexpected permission request');
    }
    return { outcome };
  }
}

// Use in tests
const mockClient = new MockClient();
mockClient.permissionResponses.set('call_1', {
  outcome: 'selected',
  optionId: 'allow',
});

Common Integration Patterns

MCP Server Integration

Integrate with Model Context Protocol servers:
const sessionResult = await connection.newSession({
  cwd: process.cwd(),
  mcpServers: [
    {
      name: 'filesystem',
      command: 'npx',
      args: ['-y', '@modelcontextprotocol/server-filesystem', workspaceRoot],
    },
    {
      name: 'github',
      command: 'npx',
      args: ['-y', '@modelcontextprotocol/server-github'],
      env: {
        GITHUB_TOKEN: process.env.GITHUB_TOKEN,
      },
    },
  ],
});

Multi-Session Management

Handle multiple concurrent sessions:
class SessionManager {
  private sessions: Map<string, Session> = new Map();
  
  async create(params: NewSessionRequest): Promise<Session> {
    const session = new Session(params);
    this.sessions.set(session.id, session);
    return session;
  }
  
  get(sessionId: string): Session | null {
    return this.sessions.get(sessionId) ?? null;
  }
  
  async close(sessionId: string): Promise<void> {
    const session = this.sessions.get(sessionId);
    if (session) {
      await session.cleanup();
      this.sessions.delete(sessionId);
    }
  }
}

Additional Resources

Gemini CLI Source

View the complete Gemini CLI Agent implementation

Protocol Specification

Deep dive into the ACP protocol details

API Reference

Complete TypeScript SDK documentation

Community Examples

Share and discover community implementations

Contributing Your Examples

Built something with ACP? We’d love to feature it! Share your implementation:
  1. Open a GitHub Discussion
  2. Include a link to your code
  3. Describe what makes your implementation unique
  4. Share any lessons learned
Outstanding community examples may be featured in the official documentation.

Build docs developers (and LLMs) love