Skip to main content

Overview

The Client interface defines the methods your client must implement to handle incoming requests from the agent. There are two required methods and several optional ones depending on the capabilities you advertise.

Required Methods

requestPermission()

Requests permission from the user for a tool call operation.
async requestPermission(
  params: RequestPermissionRequest
): Promise<RequestPermissionResponse>
params.sessionId
string
The session requesting permission.
params.toolCall
ToolCall
Information about the tool call requiring permission, including:
  • toolCallId: Unique identifier
  • title: Human-readable description
  • content: Tool call details
params.options
PermissionOption[]
Available permission options to present to the user:
  • optionId: Unique identifier for this option
  • name: Display name (e.g., “Allow”, “Deny”, “Always Allow”)
  • kind: Type of option ("allow", "deny", "apply_patch", "modify_request")
  • data: Additional data for the option (e.g., modified patch content)
outcome
RequestPermissionOutcome
required
The user’s decision:
  • { outcome: "selected", optionId: string }: User selected an option
  • { outcome: "cancelled" }: User cancelled the prompt (sent after client calls session/cancel)

Example Implementation

class MyClient implements acp.Client {
  async requestPermission(
    params: acp.RequestPermissionRequest
  ): Promise<acp.RequestPermissionResponse> {
    // Display permission prompt to user
    console.log(`\n🔐 Permission requested: ${params.toolCall.title}`);
    console.log("\nOptions:");
    
    params.options.forEach((option, index) => {
      console.log(`  ${index + 1}. ${option.name} (${option.kind})`);
    });

    // Get user input
    const answer = await promptUser("Choose an option: ");
    const optionIndex = parseInt(answer) - 1;
    
    if (optionIndex >= 0 && optionIndex < params.options.length) {
      return {
        outcome: {
          outcome: "selected",
          optionId: params.options[optionIndex].optionId,
        },
      };
    }
    
    // Invalid input - could retry or cancel
    throw new Error("Invalid option selected");
  }
}
If the client cancels the prompt turn via session/cancel, it MUST respond to any pending requestPermission request with { outcome: { outcome: "cancelled" } }.

sessionUpdate()

Handles real-time session update notifications from the agent.
async sessionUpdate(
  params: SessionNotification
): Promise<void>
params.sessionId
string
The session that generated this update.
params.update
SessionUpdate
The update content. The sessionUpdate field discriminates the type:
  • "agent_message_chunk": Streaming agent response
  • "agent_thought_chunk": Agent’s internal reasoning
  • "tool_call": New tool call initiated
  • "tool_call_update": Tool call status changed
  • "plan": Execution plan
  • And more…
This is a notification (no response expected). See Handling Session Updates for detailed implementation.

Example Implementation

src/acp.ts:45-72
class MyClient implements acp.Client {
  async sessionUpdate(params: acp.SessionNotification): Promise<void> {
    const update = params.update;

    switch (update.sessionUpdate) {
      case "agent_message_chunk":
        if (update.content.type === "text") {
          process.stdout.write(update.content.text);
        }
        break;
        
      case "tool_call":
        console.log(`\n🔧 ${update.title} (${update.status})`);
        break;
        
      case "tool_call_update":
        console.log(`🔧 Tool ${update.toolCallId}: ${update.status}`);
        break;
        
      case "plan":
        console.log("📋 Plan:", update.description);
        break;
        
      default:
        console.log(`[${update.sessionUpdate}]`);
    }
  }
}

Optional Methods

These methods are only required if you advertise the corresponding capabilities during initialization.

readTextFile()

Reads content from a text file in the client’s file system.
Only implement if you advertise fs.readTextFile: true in client capabilities.
async readTextFile(
  params: ReadTextFileRequest
): Promise<ReadTextFileResponse>
params.sessionId
string
The session making the request.
params.uri
string
Absolute file path to read.
content
string
required
The file contents as a UTF-8 string.

Example Implementation

import { readFile } from "node:fs/promises";

class MyClient implements acp.Client {
  async readTextFile(
    params: acp.ReadTextFileRequest
  ): Promise<acp.ReadTextFileResponse> {
    // Security: Validate path is within allowed directories
    if (!isPathAllowed(params.uri)) {
      throw new Error(`Access denied: ${params.uri}`);
    }
    
    try {
      const content = await readFile(params.uri, "utf-8");
      return { content };
    } catch (error) {
      throw new Error(`Failed to read file: ${error.message}`);
    }
  }
}

writeTextFile()

Writes content to a text file in the client’s file system.
Only implement if you advertise fs.writeTextFile: true in client capabilities.
async writeTextFile(
  params: WriteTextFileRequest
): Promise<WriteTextFileResponse>
params.sessionId
string
The session making the request.
params.uri
string
Absolute file path to write.
params.content
string
The content to write (UTF-8).
(empty)
object
Returns an empty object on success.

Example Implementation

import { writeFile, mkdir } from "node:fs/promises";
import { dirname } from "node:path";

class MyClient implements acp.Client {
  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    // Security: Validate path
    if (!isPathAllowed(params.uri)) {
      throw new Error(`Access denied: ${params.uri}`);
    }
    
    try {
      // Ensure directory exists
      await mkdir(dirname(params.uri), { recursive: true });
      
      // Write file
      await writeFile(params.uri, params.content, "utf-8");
      
      return {};
    } catch (error) {
      throw new Error(`Failed to write file: ${error.message}`);
    }
  }
}
See File System Operations for more details.

createTerminal()

Creates a new terminal to execute a command.
Only implement if you advertise terminal: true in client capabilities.
async createTerminal(
  params: CreateTerminalRequest
): Promise<CreateTerminalResponse>
params.sessionId
string
The session creating the terminal.
params.command
string
The command to execute.
params.args
string[]
Command arguments.
params.cwd
string
Working directory for the command.
params.env
Record<string, string>
Environment variables.
terminalId
string
required
Unique identifier for this terminal.

terminalOutput()

Gets the current output and exit status of a terminal.
async terminalOutput(
  params: TerminalOutputRequest
): Promise<TerminalOutputResponse>
params.sessionId
string
The session ID.
params.terminalId
string
The terminal ID.
output
string
The terminal output (combined stdout/stderr).
exitStatus
ExitStatus
Exit status if the command has completed:
  • { type: "exit_code", code: number }
  • { type: "signal", signal: string }

waitForTerminalExit()

Waits for a terminal command to exit and returns its exit status.
async waitForTerminalExit(
  params: WaitForTerminalExitRequest
): Promise<WaitForTerminalExitResponse>

killTerminal()

Kills a terminal command without releasing the terminal.
async killTerminal(
  params: KillTerminalRequest
): Promise<KillTerminalResponse | void>

releaseTerminal()

Releases a terminal and frees all associated resources.
async releaseTerminal(
  params: ReleaseTerminalRequest
): Promise<ReleaseTerminalResponse | void>
See Terminal Operations for detailed implementations.

Extension Methods

extMethod()

Handles arbitrary requests not part of the ACP spec.
async extMethod(
  method: string,
  params: Record<string, unknown>
): Promise<Record<string, unknown>>
Prefix extension methods with a unique identifier to avoid conflicts.

extNotification()

Handles arbitrary notifications not part of the ACP spec.
async extNotification(
  method: string,
  params: Record<string, unknown>
): Promise<void>

Complete Example

Here’s a complete client implementation:
import * as acp from "@agentclientprotocol/acp";
import { readFile, writeFile } from "node:fs/promises";

class MyClient implements acp.Client {
  // Required: Handle permission requests
  async requestPermission(
    params: acp.RequestPermissionRequest
  ): Promise<acp.RequestPermissionResponse> {
    const selectedOption = await showPermissionDialog(params);
    return {
      outcome: {
        outcome: "selected",
        optionId: selectedOption,
      },
    };
  }

  // Required: Handle session updates
  async sessionUpdate(params: acp.SessionNotification): Promise<void> {
    updateUI(params.update);
  }

  // Optional: File system support
  async readTextFile(
    params: acp.ReadTextFileRequest
  ): Promise<acp.ReadTextFileResponse> {
    const content = await readFile(params.uri, "utf-8");
    return { content };
  }

  async writeTextFile(
    params: acp.WriteTextFileRequest
  ): Promise<acp.WriteTextFileResponse> {
    await writeFile(params.uri, params.content, "utf-8");
    return {};
  }

  // Optional: Terminal support
  async createTerminal(
    params: acp.CreateTerminalRequest
  ): Promise<acp.CreateTerminalResponse> {
    const terminalId = await createTerminalSession(params);
    return { terminalId };
  }

  async terminalOutput(
    params: acp.TerminalOutputRequest
  ): Promise<acp.TerminalOutputResponse> {
    const { output, exitStatus } = getTerminalOutput(params.terminalId);
    return { output, exitStatus };
  }

  async releaseTerminal(
    params: acp.ReleaseTerminalRequest
  ): Promise<void> {
    await cleanupTerminal(params.terminalId);
  }
}

See Also

Build docs developers (and LLMs) love