A Client in ACP is the user-facing component, typically a code editor or IDE, that connects to AI agents and manages the user interaction. Clients implement the Client interface to handle requests from agents.
To create a client, implement the Client interface and set up a ClientSideConnection:
import 'dart:io';import 'package:acp_dart/acp_dart.dart';class MyClient implements Client { @override Future<RequestPermissionResponse> requestPermission( RequestPermissionRequest params, ) async { // Show permission dialog to user final choice = await showPermissionDialog(params); return RequestPermissionResponse( outcome: SelectedOutcome(optionId: choice), ); } @override Future<void> sessionUpdate(SessionNotification params) async { // Handle real-time updates from agent final update = params.update; if (update is AgentMessageChunkSessionUpdate) { displayMessageChunk(update.content); } else if (update is ToolCallSessionUpdate) { showToolCallProgress(update); } } // Implement other required methods...}Future<void> main() async { // Spawn the agent process final agentProcess = await Process.start('my-agent', []); // Create the client connection final client = MyClient(); final stream = ndJsonStream(agentProcess.stdout, agentProcess.stdin); final connection = ClientSideConnection((conn) => client, stream); // Initialize and use the connection await connection.initialize(InitializeRequest( protocolVersion: 1, clientCapabilities: ClientCapabilities( fs: FileSystemCapability( readTextFile: true, writeTextFile: true, ), terminal: true, ), ));}
Clients typically spawn agent processes and communicate via stdin/stdout, but the protocol also supports other transport mechanisms like HTTP or WebSockets.
Handles permission requests from the agent for sensitive operations.When called: When the agent needs user approval before executing a toolResponsibilities:
Present permission options to the user
Handle user’s decision
Return the selected option
Handle cancellation if user cancels the prompt turn
@overrideFuture<RequestPermissionResponse> requestPermission( RequestPermissionRequest params,) async { print('Permission requested: ${params.toolCall.title}'); // Display options to user for (int i = 0; i < params.options.length; i++) { final option = params.options[i]; print(' ${i + 1}. ${option.name}'); } // Get user input final choice = await getUserChoice(params.options); return RequestPermissionResponse( outcome: SelectedOutcome(optionId: choice.optionId), );}
If the user cancels the session while a permission request is pending, you MUST respond with CancelledOutcome() instead of hanging.
enum PermissionOptionKind { allowOnce, // Allow this specific operation allowAlways, // Allow this type of operation always rejectOnce, // Reject this specific operation rejectAlways, // Reject this type of operation always}
Receives real-time notifications from the agent about session progress.When called: Continuously during prompt processingCharacteristics:
This is a notification (no response expected)
Called frequently during agent operations
Must handle all update types
@overrideFuture<void> sessionUpdate(SessionNotification params) async { final update = params.update; switch (update) { case AgentMessageChunkSessionUpdate(): // Display agent's response text appendToChat(update.content); case AgentThoughtChunkSessionUpdate(): // Display agent's internal thinking (optional) showThought(update.content); case ToolCallSessionUpdate(): // Show new tool call displayToolCall( id: update.toolCallId, title: update.title, kind: update.kind, status: update.status, ); case ToolCallUpdateSessionUpdate(): // Update existing tool call updateToolCall( id: update.toolCallId, status: update.status, content: update.content, ); case PlanSessionUpdate(): // Display agent's execution plan showPlan(update.entries); case CurrentModeUpdateSessionUpdate(): // Agent changed modes updateCurrentMode(update.currentModeId); case AvailableCommandsUpdateSessionUpdate(): // Update available slash commands updateCommands(update.availableCommands); case ConfigOptionUpdate(): // Configuration changed updateConfig(update.configOptions); case SessionInfoUpdate(): // Session metadata updated updateSessionInfo( title: update.title, updatedAt: update.updatedAt, ); case UsageUpdate(): // Token usage update displayUsage( used: update.used, size: update.size, cost: update.cost, ); }}
Clients SHOULD continue accepting tool call updates even after sending a session/cancel notification, as the agent may send final updates before responding with the cancelled stop reason.
// Cancel an ongoing promptawait connection.cancel( CancelNotification(sessionId: session.sessionId),);// The agent will respond with StopReason.cancelled
Use locations from tool calls to implement “follow-along” behavior:
void handleToolCall(ToolCallSessionUpdate update) { if (update.locations != null && update.locations!.isNotEmpty) { final location = update.locations!.first; // Open the file in the editor openFile(location.path); // Jump to specific line if provided if (location.line != null) { scrollToLine(location.line!); highlightLine(location.line!); } }}