Skip to main content

Overview

This guide shows you how to build a basic ACP agent from scratch. An agent implements the Agent interface to handle client requests, manage sessions, and process user prompts.

Complete Example

Here’s a fully functional agent implementation that demonstrates core ACP concepts:
import 'dart:io';
import 'dart:async';
import 'dart:math';
import 'package:acp_dart/acp_dart.dart';

/// Tracks the state of an agent session including any pending operations
class AgentSession {
  /// Controller for aborting pending operations
  Completer<void>? pendingPrompt;

  AgentSession({this.pendingPrompt});
}

/// Example agent implementation demonstrating ACP protocol usage
class ExampleAgent implements Agent {
  final AgentSideConnection _connection;
  final Map<String, AgentSession> _sessions = {};

  ExampleAgent(this._connection);

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

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

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

  String _generateRandomSessionId() {
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    final random = Random();
    final buffer = StringBuffer();

    for (int i = 0; i < 16; i++) {
      buffer.write(chars[random.nextInt(chars.length)]);
    }

    return buffer.toString();
  }

  @override
  Future<PromptResponse> prompt(PromptRequest params) async {
    final session = _sessions[params.sessionId];

    if (session == null) {
      throw RequestError.resourceNotFound(params.sessionId);
    }

    // Cancel any existing pending prompt
    if (session.pendingPrompt != null && !session.pendingPrompt!.isCompleted) {
      session.pendingPrompt!.complete();
    }

    // Create a new completer for this prompt
    final completer = Completer<void>();
    session.pendingPrompt = completer;

    try {
      await _simulateTurn(params.sessionId, completer.future.asStream());
    } catch (err) {
      if (session.pendingPrompt != null && session.pendingPrompt!.isCompleted) {
        return PromptResponse(stopReason: StopReason.cancelled);
      }
      rethrow;
    } finally {
      session.pendingPrompt = null;
    }

    return PromptResponse(stopReason: StopReason.endTurn);
  }

  @override
  Future<void> cancel(CancelNotification params) async {
    final session = _sessions[params.sessionId];
    if (session != null &&
        session.pendingPrompt != null &&
        !session.pendingPrompt!.isCompleted) {
      session.pendingPrompt!.complete();
    }
  }

  // Other required methods...
  @override
  Future<LoadSessionResponse>? loadSession(LoadSessionRequest params) async {
    throw RequestError.methodNotFound('session/load');
  }

  @override
  Future<SetSessionModeResponse?>? setSessionMode(
    SetSessionModeRequest params,
  ) async {
    return SetSessionModeResponse();
  }

  @override
  Future<SetSessionConfigOptionResponse>? setSessionConfigOption(
    SetSessionConfigOptionRequest params,
  ) {
    return null;
  }

  @override
  Future<AuthenticateResponse?>? authenticate(AuthenticateRequest params) async {
    return AuthenticateResponse();
  }

  @override
  Future<SetSessionModelResponse?>? setSessionModel(
    SetSessionModelRequest params,
  ) async {
    return SetSessionModelResponse();
  }

  @override
  Future<Map<String, dynamic>>? extMethod(
    String method,
    Map<String, dynamic> params,
  ) async {
    throw RequestError.methodNotFound(method);
  }

  @override
  Future<void>? extNotification(
    String method,
    Map<String, dynamic> params,
  ) async {}

  Future<void> _simulateTurn(String sessionId, Stream<void> abortStream) async {
    // Implementation details...
  }
}

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

  // Create the agent connection
  final connection = AgentSideConnection(
    (conn) => ExampleAgent(conn),
    stream,
  );
}

Key Concepts

Initialization

The initialize method is called once at the start of the connection to negotiate protocol capabilities:
@override
Future<InitializeResponse> initialize(InitializeRequest params) async {
  return InitializeResponse(
    protocolVersion: 1,
    agentCapabilities: AgentCapabilities(
      loadSession: false,
      // Add other capabilities as needed
    ),
    authMethods: const [],
  );
}
The protocol version must match between client and agent. Version 1 is the current stable version.

Session Management

Sessions represent independent conversation contexts. Create a new session:
@override
Future<NewSessionResponse> newSession(NewSessionRequest params) async {
  final sessionId = _generateRandomSessionId();
  _sessions[sessionId] = AgentSession();

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

Processing Prompts

The prompt method handles user input and generates responses:
@override
Future<PromptResponse> prompt(PromptRequest params) async {
  final session = _sessions[params.sessionId];

  if (session == null) {
    throw RequestError.resourceNotFound(params.sessionId);
  }

  // Process the prompt and generate a response
  await _processPrompt(params);

  return PromptResponse(stopReason: StopReason.endTurn);
}

Sending Updates to the Client

Use the connection to send real-time updates:
Future<void> _sendTextUpdate(String sessionId, String text) async {
  await _connection.sessionUpdate(
    SessionNotification(
      sessionId: sessionId,
      update: AgentMessageChunkSessionUpdate(
        content: TextContentBlock(text: text),
      ),
    ),
  );
}

Tool Call Updates

Report tool execution progress:
// Create a 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 the tool call status
await _connection.sessionUpdate(
  SessionNotification(
    sessionId: sessionId,
    update: ToolCallUpdateSessionUpdate(
      toolCallId: "call_1",
      status: ToolCallStatus.completed,
      content: [
        ContentToolCallContent(
          content: TextContentBlock(text: "File contents here..."),
        ),
      ],
    ),
  ),
);

Cancellation Handling

Implement graceful cancellation:
@override
Future<void> cancel(CancelNotification params) async {
  final session = _sessions[params.sessionId];
  if (session?.pendingPrompt != null) {
    session!.pendingPrompt!.complete();
  }
}
Always check if a completer is already completed before calling complete() to avoid exceptions.

Error Handling

Use RequestError for protocol-level errors:
// Resource not found
throw RequestError.resourceNotFound(sessionId);

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

// Authentication required
throw RequestError.authRequired();

// Invalid parameters
throw RequestError.invalidParams({'field': 'sessionId'});

// Internal error
throw RequestError.internalError('Something went wrong');

Running the Agent

The agent runs as a stdio-based process:
void main() {
  final stream = ndJsonStream(stdin, stdout);
  final connection = AgentSideConnection(
    (conn) => ExampleAgent(conn),
    stream,
  );
  
  // The connection handles all incoming requests automatically
  // Keep the program running
}

Next Steps

Basic Client

Learn how to create a client that connects to your agent

Advanced Usage

Explore advanced patterns like permissions and protocol cancellation

Core Concepts

Understand the underlying architecture

API Reference

Explore the complete Agent interface

Build docs developers (and LLMs) love