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
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
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:
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.
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:
// 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:
Stream ends normally - Agent process exits or stream is closed
Error occurs - Network error, parse error, or protocol violation
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: "..." });
}
}
{
"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:
// 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
Handle Connection Closure
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