Skip to main content
Connections are the foundation of communication in ACP. The SDK provides two connection classes that handle bidirectional message passing between agents and clients.

Connection Classes

The SDK provides two connection classes, one for each side:

AgentSideConnection

Used by agents to communicate with clients

ClientSideConnection

Used by clients to communicate with agents

AgentSideConnection

The AgentSideConnection class provides the agent’s view of an ACP connection.

Creating a Connection

src/acp.ts
import { AgentSideConnection, ndJsonStream, Agent } from "@anoma/acp-sdk";

class MyAgent implements Agent {
  constructor(private connection: AgentSideConnection) {}
  
  async initialize(params) {
    // Implementation
  }
  
  // ... other Agent methods
}

// Create stream from stdio
const stream = ndJsonStream(
  Deno.stdout.writable,
  Deno.stdin.readable
);

// Create connection
const connection = new AgentSideConnection(
  (conn) => new MyAgent(conn),
  stream
);

// Wait for connection to close
await connection.closed;
The agent factory function receives the connection instance, allowing your agent to send requests back to the client.

Agent → Client Methods

The connection provides methods for agents to communicate with clients:
// Send session updates (notification)
await connection.sessionUpdate({
  sessionId: "session-123",
  content: {
    type: "text",
    text: "Processing your request..."
  }
});

// Request permission from user
const response = await connection.requestPermission({
  sessionId: "session-123",
  toolCallId: "call-1",
  options: [
    { id: "allow", kind: "allow", label: "Allow" },
    { id: "deny", kind: "deny", label: "Deny" }
  ]
});

// Read a file from client's filesystem
const fileContent = await connection.readTextFile({
  sessionId: "session-123",
  path: "/path/to/file.txt"
});

// Write a file to client's filesystem
await connection.writeTextFile({
  sessionId: "session-123",
  path: "/path/to/output.txt",
  content: "Hello, world!"
});

// Create a terminal
const terminal = await connection.createTerminal({
  sessionId: "session-123",
  command: "npm",
  args: ["test"],
  cwd: "/project"
});

ClientSideConnection

The ClientSideConnection class provides the client’s view of an ACP connection.

Creating a Connection

src/acp.ts
import { ClientSideConnection, ndJsonStream, Client } from "@anoma/acp-sdk";

class MyClient implements Client {
  async requestPermission(params) {
    // Show UI and get user choice
  }
  
  async sessionUpdate(params) {
    // Display in UI
  }
  
  // ... other Client methods
}

// Create stream connected to agent process
const agentProcess = new Deno.Command("./my-agent", {
  stdin: "piped",
  stdout: "piped"
}).spawn();

const stream = ndJsonStream(
  agentProcess.stdin,
  agentProcess.stdout
);

// Create connection
const agent = new ClientSideConnection(
  () => new MyClient(),
  stream
);

// Now you can call agent methods
const initResponse = await agent.initialize({
  protocolVersion: 1,
  capabilities: { fs: { readTextFile: true } },
  clientInfo: { name: "My Editor", version: "1.0.0" }
});

Client → Agent Methods

// Initialize the connection
const initResponse = await agent.initialize({
  protocolVersion: 1,
  capabilities: {
    fs: { readTextFile: true, writeTextFile: true },
    terminal: true
  },
  clientInfo: {
    name: "My Editor",
    version: "1.0.0"
  }
});

// Create a new session
const session = await agent.newSession({
  cwd: "/project",
  mcpServers: []
});

// Send a prompt
const response = await agent.prompt({
  sessionId: session.sessionId,
  messages: [
    {
      role: "user",
      content: { type: "text", text: "Help me fix this bug" }
    }
  ]
});

// Cancel an ongoing operation
await agent.cancel({
  sessionId: session.sessionId
});

// Load an existing session
await agent.loadSession({
  sessionId: "existing-session-id",
  mcpServers: []
});

Stream-Based Communication

Both connection classes use the Stream interface for message passing:
src/stream.ts
export type Stream = {
  writable: WritableStream<AnyMessage>;
  readable: ReadableStream<AnyMessage>;
};

Creating Streams

The SDK provides ndJsonStream() to create streams from stdin/stdout:
import { ndJsonStream } from "@anoma/acp-sdk";

// Agent: Use stdin/stdout
const stream = ndJsonStream(
  Deno.stdout.writable,
  Deno.stdin.readable
);

// Client: Connect to spawned agent process
const agentProcess = new Deno.Command("./agent", {
  stdin: "piped",
  stdout: "piped"
}).spawn();

const stream = ndJsonStream(
  agentProcess.stdin,
  agentProcess.stdout
);
Messages are sent as newline-delimited JSON (NDJSON), with each message on a single line.

Message Format

All messages follow the JSON-RPC 2.0 format:
// Request (expects response)
{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "session/prompt",
  "params": { "sessionId": "abc", "messages": [...] }
}

// Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": { "stopReason": "endTurn" }
}

// Notification (no response)
{
  "jsonrpc": "2.0",
  "method": "session/update",
  "params": { "sessionId": "abc", "content": {...} }
}

Connection Lifecycle

Connections have a well-defined lifecycle with monitoring capabilities:

Initialization Flow

Monitoring Connection Status

Both connection classes provide properties to monitor connection status:
src/acp.ts
// AbortSignal that fires when connection closes
connection.signal.addEventListener('abort', () => {
  console.log('Connection closed');
});

// Check if connection is closed (synchronous)
if (connection.signal.aborted) {
  console.log('Connection is already closed');
}

// Wait for connection to close (async)
await connection.closed;
console.log('Connection closed - cleanup complete');

// Use signal with other APIs
fetch(url, { signal: connection.signal });
setTimeout(() => {}, 1000, { signal: connection.signal });
Once a connection is closed, it cannot be reopened. You must create a new connection.

Closing Connections

Connections close when:
  1. Stream ends normally - Agent process exits or stream is closed
  2. Error occurs - Network error, parse error, or protocol violation
  3. Abort signal triggered - External cancellation
// Agent: Connection closes when process exits
await connection.closed;
Deno.exit(0);

// Client: Connection closes when agent process exits
const connection = new ClientSideConnection(client, stream);
try {
  await connection.closed;
} catch (error) {
  console.error('Connection error:', error);
}

Error Handling

Connections handle errors according to JSON-RPC 2.0 specification:
import { RequestError } from "@anoma/acp-sdk";

class MyAgent implements Agent {
  async prompt(params) {
    // Return standard error responses
    if (!sessionExists(params.sessionId)) {
      throw RequestError.invalidParams(
        { sessionId: params.sessionId },
        "Session not found"
      );
    }
    
    // Other built-in error types:
    throw RequestError.methodNotFound("unknown/method");
    throw RequestError.authRequired();
    throw RequestError.internalError({ details: "..." });
  }
}

Error Response Format

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32602,
    "message": "Invalid params: Session not found",
    "data": { "sessionId": "invalid-id" }
  }
}

Extension Methods

Both connection classes support custom methods beyond the ACP specification:
// Agent sends custom request
const result = await connection.extMethod("myeditor.showPanel", {
  panelId: "debugger"
});

// Agent sends custom notification
await connection.extNotification("myeditor.highlight", {
  line: 42
});

// Client sends custom request to agent
const result = await agent.extMethod("myagent.getStats", {});

// Client sends custom notification to agent
await agent.extNotification("myagent.clearCache", {});
Extension methods should be prefixed with a unique identifier to avoid conflicts (e.g., domain name).

Terminal Handling

Agents can create and manage terminals through the connection:
src/acp.ts
// Create a terminal
const terminal = await connection.createTerminal({
  sessionId,
  command: "npm",
  args: ["test"],
  cwd: "/project"
});

// Get current output (non-blocking)
const output = await terminal.currentOutput();
console.log(output.output);

// Wait for command to complete
const exitStatus = await terminal.waitForExit();
if (exitStatus.exitCode === 0) {
  console.log("Command succeeded");
}

// Kill the command
await terminal.kill();

// Always release when done
await terminal.release();

// Or use automatic cleanup
await using terminal = await connection.createTerminal({...});
// Automatically released when out of scope
Always call release() on terminals to free resources, or use await using for automatic cleanup.

Best Practices

Always monitor connection status and handle cleanup:
connection.signal.addEventListener('abort', () => {
  // Clean up resources
  closeDatabase();
  saveState();
});
The SDK automatically validates messages using Zod schemas. Validation errors are returned as JSON-RPC errors.
Send frequent updates to keep users informed:
await connection.sessionUpdate({
  sessionId,
  content: { type: "text", text: "Analyzing code..." }
});

// Do work

await connection.sessionUpdate({
  sessionId,
  content: { type: "text", text: "Generating fix..." }
});
Pass the connection signal to operations for automatic cancellation:
await fetch(url, { signal: connection.signal });

Learn More

Protocol Overview

Understanding the ACP specification

Agents and Clients

Learn about the two sides of ACP

Sessions

Managing conversation contexts

Build an Agent

Complete agent implementation guide

Build docs developers (and LLMs) love