The ACP TypeScript SDK allows you to extend the protocol with custom methods and notifications that aren’t part of the core specification. This is useful for:
- Adding proprietary features to your agent or client
- Implementing experimental capabilities
- Integrating with custom systems
Overview
Both sides of the connection support extension methods:
- Agent side: Implement
extMethod and extNotification in your Agent implementation
- Client side: Implement
extMethod and extNotification in your Client implementation
Extension methods and notifications work just like standard protocol methods, using the same JSON-RPC 2.0 message format.
Agent Extension Methods
Agents can handle custom requests from clients by implementing the optional extMethod and extNotification methods:
Receiving Extension Methods
import * as acp from '@agentclientprotocol/acp';
class MyAgent implements acp.Agent {
// ... standard methods ...
/**
* Handle custom requests from the client
*/
async extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
switch (method) {
case 'example.com/get_agent_stats':
return {
uptime: process.uptime(),
sessionsActive: this.sessions.size,
version: '1.0.0',
};
case 'example.com/custom_command':
const command = params.command as string;
return { result: await this.runCustomCommand(command) };
default:
throw acp.RequestError.methodNotFound(method);
}
}
/**
* Handle custom notifications from the client
*/
async extNotification(
method: string,
params: Record<string, unknown>,
): Promise<void> {
switch (method) {
case 'example.com/log_event':
console.log('Client event:', params);
break;
case 'example.com/cache_invalidate':
this.clearCache();
break;
default:
// Silently ignore unknown notifications
console.warn('Unknown notification:', method);
}
}
}
Sending Extension Methods (Agent to Client)
Agents can call custom methods on the client using AgentSideConnection:
class MyAgent implements acp.Agent {
private connection: acp.AgentSideConnection;
constructor(connection: acp.AgentSideConnection) {
this.connection = connection;
}
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
// Call a custom client method
const response = await this.connection.extMethod(
'example.com/get_workspace_info',
{ includeGitStatus: true }
);
console.log('Workspace info:', response);
// Send a custom notification
await this.connection.extNotification(
'example.com/agent_thinking',
{ status: 'analyzing code' }
);
// ... rest of prompt handling ...
}
}
Client Extension Methods
Clients can handle custom requests from agents similarly:
Receiving Extension Methods
import * as acp from '@agentclientprotocol/acp';
class MyClient implements acp.Client {
// ... standard methods ...
/**
* Handle custom requests from the agent
*/
async extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
switch (method) {
case 'example.com/get_workspace_info':
return {
path: process.cwd(),
gitBranch: await this.getGitBranch(),
openFiles: this.getOpenFiles(),
};
case 'example.com/show_notification':
this.showNotification(params.message as string);
return { shown: true };
default:
throw acp.RequestError.methodNotFound(method);
}
}
/**
* Handle custom notifications from the agent
*/
async extNotification(
method: string,
params: Record<string, unknown>,
): Promise<void> {
switch (method) {
case 'example.com/agent_thinking':
this.updateStatusBar(params.status as string);
break;
case 'example.com/telemetry':
this.sendTelemetry(params);
break;
default:
console.warn('Unknown notification:', method);
}
}
}
Sending Extension Methods (Client to Agent)
const connection = new acp.ClientSideConnection(
(agent) => new MyClient(),
stream
);
// Initialize and create session...
// Call a custom agent method
const stats = await connection.extMethod(
'example.com/get_agent_stats',
{}
);
console.log('Agent stats:', stats);
// Send a custom notification
await connection.extNotification(
'example.com/log_event',
{ event: 'user_action', action: 'file_saved' }
);
Naming Conventions
To avoid conflicts with future protocol additions and other implementations, follow these naming conventions:
Use Reverse Domain Names
Prefix your extension methods with a reverse domain name:
// Good
'example.com/get_stats'
'mycompany.io/custom_feature'
'github.com/copilot/analyze'
// Bad (may conflict with future ACP methods)
'get_stats'
'custom_feature'
'analyze'
Use Descriptive Names
Method names should clearly indicate their purpose:
// Good
'example.com/workspace/get_git_status'
'example.com/editor/show_notification'
'example.com/telemetry/log_event'
// Less clear
'example.com/get'
'example.com/do_thing'
'example.com/update'
Type Safety with Extensions
For better type safety, define interfaces for your extension methods:
// Define request/response types
interface GetAgentStatsRequest {
includeMemory?: boolean;
}
interface GetAgentStatsResponse {
uptime: number;
sessionsActive: number;
version: string;
memoryUsage?: {
heapUsed: number;
heapTotal: number;
};
}
// Use them in your implementation
class MyAgent implements acp.Agent {
async extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
switch (method) {
case 'example.com/get_agent_stats': {
const req = params as GetAgentStatsRequest;
const response: GetAgentStatsResponse = {
uptime: process.uptime(),
sessionsActive: this.sessions.size,
version: '1.0.0',
};
if (req.includeMemory) {
const mem = process.memoryUsage();
response.memoryUsage = {
heapUsed: mem.heapUsed,
heapTotal: mem.heapTotal,
};
}
return response;
}
default:
throw acp.RequestError.methodNotFound(method);
}
}
}
Complete Example
Here’s a complete example showing bidirectional extension methods:
import * as acp from '@agentclientprotocol/acp';
class ExtendedAgent implements acp.Agent {
private connection: acp.AgentSideConnection;
constructor(connection: acp.AgentSideConnection) {
this.connection = connection;
}
// Standard methods...
async initialize(params: acp.InitializeRequest) { /* ... */ }
async newSession(params: acp.NewSessionRequest) { /* ... */ }
async authenticate(params: acp.AuthenticateRequest) { /* ... */ }
async prompt(params: acp.PromptRequest): Promise<acp.PromptResponse> {
// Get workspace context using extension method
const workspace = await this.connection.extMethod(
'example.com/get_workspace',
{ includeGit: true }
);
// Process prompt with workspace context...
return { stopReason: 'end_turn' };
}
// Extension methods
async extMethod(
method: string,
params: Record<string, unknown>,
): Promise<Record<string, unknown>> {
if (method === 'example.com/get_capabilities') {
return {
supportsStreaming: true,
supportsMultiModal: true,
customFeatures: ['workspace-analysis', 'git-integration'],
};
}
throw acp.RequestError.methodNotFound(method);
}
}
Best Practices
- Always use namespaced method names to avoid conflicts
- Document your extensions clearly for users of your implementation
- Handle unknown methods gracefully by throwing
RequestError.methodNotFound
- Version your extension methods if you expect them to change
- Consider backward compatibility when modifying extension method behavior