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