Skip to main content
This guide walks you through building an ACP agent that can handle client requests, manage sessions, and process prompts.

Overview

An agent is the AI-powered side of the ACP connection that:
  • Receives prompts from clients
  • Processes requests using language models
  • Executes tool calls
  • Sends session updates back to clients

Prerequisites

Add the ACP Dart SDK to your pubspec.yaml:
dependencies:
  acp_dart: ^1.0.0

Implementation Steps

1
Create your Agent class
2
Implement the Agent abstract class to handle all protocol methods:
3
import 'package:acp_dart/acp_dart.dart';

class MyAgent implements Agent {
  final AgentSideConnection _connection;
  final Map<String, AgentSession> _sessions = {};

  MyAgent(this._connection);

  @override
  Future<InitializeResponse> initialize(InitializeRequest params) async {
    return InitializeResponse(
      protocolVersion: 1,
      agentCapabilities: AgentCapabilities(
        loadSession: false,
        sessionCapabilities: SessionCapabilities(),
      ),
      authMethods: const [],
    );
  }

  @override
  Future<NewSessionResponse> newSession(NewSessionRequest params) async {
    final sessionId = _generateSessionId();
    _sessions[sessionId] = AgentSession();

    return NewSessionResponse(
      sessionId: sessionId,
      modes: SessionModeState(
        availableModes: [
          SessionMode(id: 'default', name: 'Default'),
          SessionMode(id: 'code', name: 'Code Mode'),
        ],
        currentModeId: 'default',
      ),
    );
  }

  // Additional required methods...
}
4
Handle prompt requests
5
Implement the prompt method to process user input and generate responses:
6
@override
Future<PromptResponse> prompt(PromptRequest params) async {
  final session = _sessions[params.sessionId];
  
  if (session == null) {
    throw RequestError.resourceNotFound(params.sessionId);
  }

  // Send initial response chunk
  await _connection.sessionUpdate(
    SessionNotification(
      sessionId: params.sessionId,
      update: AgentMessageChunkSessionUpdate(
        content: TextContentBlock(
          text: 'Processing your request...',
        ),
      ),
    ),
  );

  // Process the prompt with your AI model
  // Send more updates as needed

  return PromptResponse(stopReason: StopReason.endTurn);
}
7
Send tool call updates
8
Notify clients about tool execution progress:
9
// Report a new tool call
await _connection.sessionUpdate(
  SessionNotification(
    sessionId: sessionId,
    update: ToolCallSessionUpdate(
      toolCallId: 'call_1',
      title: 'Reading project files',
      kind: ToolKind.read,
      status: ToolCallStatus.pending,
      locations: [
        ToolCallLocation(path: '/project/README.md'),
      ],
      rawInput: {'path': '/project/README.md'},
    ),
  ),
);

// Update when complete
await _connection.sessionUpdate(
  SessionNotification(
    sessionId: sessionId,
    update: ToolCallUpdateSessionUpdate(
      toolCallId: 'call_1',
      status: ToolCallStatus.completed,
      content: [
        ContentToolCallContent(
          content: TextContentBlock(
            text: '# My Project\nReadme contents...',
          ),
        ),
      ],
      rawOutput: {'content': '# My Project\nReadme contents...'},
    ),
  ),
);
10
Request permissions for sensitive operations
11
Use requestPermission for operations that need user approval:
12
final permissionResponse = await _connection.requestPermission(
  RequestPermissionRequest(
    sessionId: sessionId,
    options: [
      PermissionOption(
        optionId: 'allow',
        name: 'Allow this change',
        kind: PermissionOptionKind.allowOnce,
      ),
      PermissionOption(
        optionId: 'reject',
        name: 'Skip this change',
        kind: PermissionOptionKind.rejectOnce,
      ),
    ],
    toolCall: ToolCallUpdate(
      toolCallId: 'call_2',
      title: 'Modifying configuration file',
      kind: ToolKind.edit,
      status: ToolCallStatus.pending,
      locations: [
        ToolCallLocation(path: '/project/config.json'),
      ],
      rawInput: {
        'path': '/project/config.json',
        'content': '{"database": {"host": "new-host"}}',
      },
    ),
  ),
);

// Handle the response
final outcome = permissionResponse.outcome;
switch (outcome) {
  case SelectedOutcome(optionId: final optionId) when optionId == 'allow':
    // Execute the operation
    break;
  case SelectedOutcome(optionId: final optionId) when optionId == 'reject':
    // Skip the operation
    break;
  case CancelledOutcome():
    // Handle cancellation
    break;
}
13
Handle cancellation requests
14
Implement graceful cancellation for long-running operations:
15
@override
Future<void> cancel(CancelNotification params) async {
  final session = _sessions[params.sessionId];
  
  if (session != null && 
      session.pendingPrompt != null && 
      !session.pendingPrompt!.isCompleted) {
    session.pendingPrompt!.complete();
  }
}
16
Set up the connection
17
Create the agent connection and start listening:
18
import 'dart:io';
import 'package:acp_dart/acp_dart.dart';

void main() {
  // Create an NDJSON stream using stdin/stdout
  final stream = ndJsonStream(stdin, stdout);

  // Create the agent-side connection
  final connection = AgentSideConnection(
    (conn) => MyAgent(conn),
    stream,
  );

  // The connection automatically handles incoming messages
}

Complete Example

See the full example in the repository:
dart run example/agent.dart

Error Handling

Always throw RequestError instances for protocol-level errors. The SDK automatically converts them to proper JSON-RPC error responses.
// Resource not found
throw RequestError.resourceNotFound('/path/to/file.txt');

// Method not implemented
throw RequestError.methodNotFound('session/load');

// Invalid parameters
throw RequestError.invalidParams({'reason': 'Session ID required'});

// Authentication required
throw RequestError.authRequired();

Next Steps

Error Handling

Learn about error handling patterns

Terminal Operations

Execute commands in terminals

Build docs developers (and LLMs) love