or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

agent-connection.mdclient-connection.mderrors.mdindex.mdinterfaces.mdprotocol-types.mdstream.md
tile.json

agent-connection.mddocs/

Agent-Side Connection

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.

Capabilities

AgentSideConnection Constructor

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 implementation
  • stream: A bidirectional stream created via ndJsonStream() or custom Stream implementation

Usage 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)
);

Session Update Notification

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 ID
  • params.kind (string): Type of update - see SessionNotification union type
  • Additional properties depend on the notification kind

Session 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 changed

Usage 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'
    }]
  }
});

Request Permission

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 ID
  • params.toolCall (object): Tool call requiring permission with toolCallId, title, etc.
  • params.options (PermissionOption[]): Available permission options

Permission Options:

  • Each option has an optionId, name, and kind
  • Common option kinds: "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".

Read Text File

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 ID
  • params.path (string): Absolute file path
  • params.line (number | undefined): Optional starting line (1-indexed)
  • params.limit (number | undefined): Optional maximum number of lines to read

Returns:

  • content (string): The file contents

Availability: 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');
  }
}

Write Text File

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 ID
  • params.path (string): Absolute file path
  • params.content (string): Content to write

Returns:

  • Empty object {} on success

Availability: 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');

Create Terminal

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 ID
  • params.command (string): Command to execute
  • params.args (string[] | undefined): Optional command arguments
  • params.env (EnvVariable[] | undefined): Optional environment variables
  • params.cwd (string | undefined): Optional working directory
  • params.outputByteLimit (number | undefined): Optional maximum output bytes to retain

Returns:

  • TerminalHandle: Object for interacting with the terminal

Availability: 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 scope

TerminalHandle 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.

Extension Method

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 parameters

Returns:

  • Arbitrary response object

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.

Extension Notification

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 parameters

Usage Example:

// Send custom notification (fire-and-forget)
await connection.extNotification('myapp.statusUpdate', {
  status: 'processing',
  progress: 0.5
});

Connection Lifecycle Properties

AbortSignal

/**
 * AbortSignal that aborts when the connection closes
 */
get signal(): AbortSignal

The signal can be used to:

  • Listen for connection closure via event listeners
  • Check if connection is closed synchronously
  • Pass to other APIs (fetch, setTimeout) for automatic cancellation

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 });

Closed Promise

/**
 * 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)
]);

Complete Example

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;

Error Handling

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);
    }
  }
}

Notes

  • All methods are async and return Promises
  • Session updates are notifications (no response expected)
  • Permission requests must be answered before the prompt completes
  • Always release terminals when done to free resources
  • The connection automatically handles JSON-RPC message routing and validation
  • Extension methods/notifications are prefixed with underscore in the protocol