The AgentSideConnection class provides the agent's view of an ACP connection, allowing agents to communicate with clients. It handles JSON-RPC message routing, validates requests using Zod schemas, and provides type-safe methods for all client operations.
Creates a new agent-side connection to a client.
/**
* Creates a new agent-side connection to a client
* @param toAgent - Factory function that creates an Agent handler
* @param stream - Bidirectional message stream for communication
*/
constructor(
toAgent: (conn: AgentSideConnection) => Agent,
stream: Stream
)Parameters:
toAgent: A factory function that receives the connection instance and returns an Agent implementationstream: A bidirectional stream created via ndJsonStream() or custom Stream implementationUsage Example:
import { AgentSideConnection, Agent, ndJsonStream } from '@agentclientprotocol/sdk';
const agent: Agent = {
// ... agent implementation
};
const connection = new AgentSideConnection(
(conn) => agent, // Factory receives connection for bi-directional communication
ndJsonStream(outputStream, inputStream)
);Sends session update notifications to the client about prompt processing progress.
/**
* Sends session update notifications from the agent
* @param params - Session notification parameters
*/
async sessionUpdate(params: SessionNotification): Promise<void>Parameters:
params.sessionId (string): The session IDparams.kind (string): Type of update - see SessionNotification union typeSession Update Types:
"user_message_chunk": Streaming user message content"agent_message_chunk": Streaming agent message content"agent_thought_chunk": Streaming agent thought content"tool_call": Tool call being executed"tool_call_update": Update to existing tool call"plan": Execution plan"available_commands_update": Updated list of available commands"current_mode_update": Session mode changedUsage Example:
// Send streaming message chunks
await connection.sessionUpdate({
sessionId: 'session-123',
update: {
sessionUpdate: 'agent_message_chunk',
content: {
type: 'text',
text: 'Here is the code...'
}
}
});
// Report tool call execution
await connection.sessionUpdate({
sessionId: 'session-123',
update: {
sessionUpdate: 'tool_call',
toolCallId: 'call-1',
title: 'Read file',
status: 'in_progress',
kind: 'read',
locations: [{
path: '/path/to/file.ts',
line: 10
}]
}
});
// Send execution plan
await connection.sessionUpdate({
sessionId: 'session-123',
update: {
sessionUpdate: 'plan',
entries: [{
content: 'Read current file',
priority: 'high',
status: 'completed'
}, {
content: 'Make changes',
priority: 'medium',
status: 'pending'
}]
}
});Requests user permission for potentially sensitive operations.
/**
* Requests permission from the user for a tool call operation
* @param params - Permission request parameters
* @returns User's permission decision
*/
async requestPermission(
params: RequestPermissionRequest
): Promise<RequestPermissionResponse>Parameters:
params.sessionId (string): The session IDparams.toolCall (object): Tool call requiring permission with toolCallId, title, etc.params.options (PermissionOption[]): Available permission optionsPermission Options:
optionId, name, and kind"allow_once", "allow_always", "reject_once", "reject_always"Returns:
outcome (object): Either { outcome: "cancelled" } or { outcome: "selected", optionId: string }Usage Example:
const response = await connection.requestPermission({
sessionId: 'session-123',
toolCall: {
toolCallId: 'call-1',
title: 'Write to src/app.ts',
status: 'pending',
kind: 'edit'
},
options: [{
optionId: 'allow',
name: 'Allow',
kind: 'allow_once'
}, {
optionId: 'reject',
name: 'Reject',
kind: 'reject_once'
}]
});
if (response.outcome.outcome === 'selected') {
// Proceed with the operation
await performAction();
} else if (response.outcome.outcome === 'cancelled') {
// Prompt turn was cancelled, abort
throw new Error('Cancelled');
}Important: If the client sends a session/cancel notification, it MUST respond to pending permission requests with outcome "cancelled".
Reads content from a text file in the client's file system.
/**
* Reads content from a text file in the client's file system
* @param params - File read parameters
* @returns File contents
*/
async readTextFile(
params: ReadTextFileRequest
): Promise<ReadTextFileResponse>Parameters:
params.sessionId (string): The session IDparams.path (string): Absolute file pathparams.line (number | undefined): Optional starting line (1-indexed)params.limit (number | undefined): Optional maximum number of lines to readReturns:
content (string): The file contentsAvailability: Only available if client advertises fs.readTextFile capability.
Usage Example:
try {
const response = await connection.readTextFile({
sessionId: 'session-123',
path: '/home/user/project/src/app.ts'
});
console.log('File contents:', response.content);
// Read specific line range
const partial = await connection.readTextFile({
sessionId: 'session-123',
path: '/home/user/project/src/app.ts',
line: 10,
limit: 10
});
} catch (error) {
if (error.code === -32002) {
console.error('File not found');
}
}Writes content to a text file in the client's file system.
/**
* Writes content to a text file in the client's file system
* @param params - File write parameters
* @returns Empty response on success
*/
async writeTextFile(
params: WriteTextFileRequest
): Promise<WriteTextFileResponse>Parameters:
params.sessionId (string): The session IDparams.path (string): Absolute file pathparams.content (string): Content to writeReturns:
{} on successAvailability: Only available if client advertises fs.writeTextFile capability.
Usage Example:
await connection.writeTextFile({
sessionId: 'session-123',
path: '/home/user/project/src/app.ts',
content: 'export const greeting = "Hello, World!";\n'
});
console.log('File written successfully');Creates a new terminal to execute a command.
/**
* Creates a new terminal to execute a command
* @param params - Terminal creation parameters
* @returns Handle to control and monitor the terminal
*/
async createTerminal(
params: CreateTerminalRequest
): Promise<TerminalHandle>Parameters:
params.sessionId (string): The session IDparams.command (string): Command to executeparams.args (string[] | undefined): Optional command argumentsparams.env (EnvVariable[] | undefined): Optional environment variablesparams.cwd (string | undefined): Optional working directoryparams.outputByteLimit (number | undefined): Optional maximum output bytes to retainReturns:
TerminalHandle: Object for interacting with the terminalAvailability: Only available if client advertises terminal: true capability.
Usage Example:
// Create and execute command
const terminal = await connection.createTerminal({
sessionId: 'session-123',
command: 'npm',
args: ['test'],
cwd: '/home/user/project',
outputByteLimit: 1000000
});
// Get current output without waiting
const output = await terminal.currentOutput();
console.log('Output so far:', output.output);
// Wait for command to complete
const exitStatus = await terminal.waitForExit();
if (exitStatus.exitCode === 0) {
console.log('Command succeeded');
} else {
console.error('Command failed with code:', exitStatus.exitCode);
}
// Always release when done
await terminal.release();
// Or use automatic cleanup with await using
await using terminal = await connection.createTerminal({
sessionId: 'session-123',
command: 'ls',
args: ['-la']
});
// Terminal automatically released when out of scopeTerminalHandle Methods:
class TerminalHandle {
id: string; // Terminal ID for embedding in tool calls
async currentOutput(): Promise<TerminalOutputResponse>;
async waitForExit(): Promise<WaitForTerminalExitResponse>;
async kill(): Promise<KillTerminalResponse>;
async release(): Promise<ReleaseTerminalResponse | void>;
async [Symbol.asyncDispose](): Promise<void>;
}The terminal ID can be used in ToolCallContent with type "terminal" to embed the terminal in session updates.
Sends an arbitrary request that is not part of the ACP spec.
/**
* Extension method for custom requests
* @param method - Custom method name (will be prefixed with underscore)
* @param params - Method parameters
* @returns Method response
*/
async extMethod(
method: string,
params: Record<string, unknown>
): Promise<Record<string, unknown>>Parameters:
method (string): Custom method name (automatically prefixed with _ in protocol)params (object): Arbitrary method parametersReturns:
Usage Example:
// Send custom request
const result = await connection.extMethod('myapp.customAction', {
data: 'some value',
options: { flag: true }
});
console.log('Custom result:', result);Best Practice: Prefix extension methods with a unique identifier (e.g., domain name) to avoid conflicts: domain.method.
Sends an arbitrary notification that is not part of the ACP spec.
/**
* Extension notification for custom events
* @param method - Custom notification name (will be prefixed with underscore)
* @param params - Notification parameters
*/
async extNotification(
method: string,
params: Record<string, unknown>
): Promise<void>Parameters:
method (string): Custom notification name (automatically prefixed with _ in protocol)params (object): Arbitrary notification parametersUsage Example:
// Send custom notification (fire-and-forget)
await connection.extNotification('myapp.statusUpdate', {
status: 'processing',
progress: 0.5
});/**
* AbortSignal that aborts when the connection closes
*/
get signal(): AbortSignalThe signal can be used to:
Usage Example:
// Listen for closure
connection.signal.addEventListener('abort', () => {
console.log('Connection closed - cleaning up');
cleanup();
});
// Check status
if (connection.signal.aborted) {
console.log('Connection is already closed');
return;
}
// Pass to other APIs for automatic cancellation
fetch(url, { signal: connection.signal });
setTimeout(() => {
console.log('Delayed action');
}, 5000, { signal: connection.signal });/**
* Promise that resolves when the connection closes
*/
get closed(): Promise<void>The connection closes when the underlying stream ends, either normally or due to an error. Once closed, no more messages can be sent or received.
Usage Example:
// Wait for connection to close
await connection.closed;
console.log('Connection closed - performing cleanup');
cleanup();
// Or use with Promise.race for timeout
await Promise.race([
connection.closed,
sleep(30000)
]);import { AgentSideConnection, Agent, ndJsonStream, RequestError } from '@agentclientprotocol/sdk';
const agent: Agent = {
async initialize(params) {
return {
protocolVersion: 1,
agentCapabilities: {
mcpCapabilities: { http: true, sse: false },
promptCapabilities: { image: true, audio: false, embeddedContext: true }
},
agentInfo: {
name: 'example-agent',
version: '1.0.0'
}
};
},
async newSession(params) {
const sessionId = crypto.randomUUID();
return {
sessionId,
modes: {
availableModes: [{
id: 'code',
name: 'Code Mode',
description: 'AI-powered code editing'
}],
currentModeId: 'code'
}
};
},
async authenticate(params) {
// Validate authentication
return {};
},
async prompt(params) {
const { sessionId, prompt } = params;
// Send plan
await connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: 'plan',
entries: [{
content: 'Analyze user request',
priority: 'high',
status: 'completed'
}, {
content: 'Generate response',
priority: 'medium',
status: 'in_progress'
}]
}
});
// Request permission for file access
const permission = await connection.requestPermission({
sessionId,
toolCall: {
toolCallId: 'call-1',
title: 'Read src/app.ts',
status: 'pending',
kind: 'read'
},
options: [{
optionId: 'allow',
name: 'Allow',
kind: 'allow_once'
}, {
optionId: 'deny',
name: 'Deny',
kind: 'reject_once'
}]
});
if (permission.outcome.outcome === 'selected') {
// Read the file
const file = await connection.readTextFile({
sessionId,
path: '/project/src/app.ts'
});
// Send tool call update
await connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: 'tool_call_update',
toolCallId: 'call-1',
status: 'completed',
content: [{
type: 'content',
content: {
type: 'text',
text: file.content
}
}]
}
});
}
// Stream response
await connection.sessionUpdate({
sessionId,
update: {
sessionUpdate: 'agent_message_chunk',
content: {
type: 'text',
text: 'Based on the code, I suggest...'
}
}
});
return {
stopReason: 'end_turn'
};
},
async cancel(params) {
// Cancel ongoing operations
console.log('Cancelling session:', params.sessionId);
}
};
// Create connection
const connection = new AgentSideConnection(
(conn) => agent,
ndJsonStream(outputStream, inputStream)
);
// Handle connection closure
connection.signal.addEventListener('abort', () => {
console.log('Connection closed');
});
// Wait for connection to close
await connection.closed;All methods can throw RequestError for protocol-level errors:
try {
await connection.readTextFile({
sessionId: 'session-123',
uri: 'file:///nonexistent.txt'
});
} catch (error) {
if (error instanceof RequestError) {
switch (error.code) {
case -32002: // Resource not found
console.error('File not found:', error.data);
break;
case -32603: // Internal error
console.error('Internal error:', error.message);
break;
default:
console.error('Request error:', error.code, error.message);
}
}
}