971 lines
38 KiB
JavaScript
971 lines
38 KiB
JavaScript
import { z } from "zod";
|
|
import * as schema from "./schema/index.js";
|
|
import * as validate from "./schema/zod.gen.js";
|
|
export * from "./schema/index.js";
|
|
export * from "./stream.js";
|
|
/**
|
|
* An agent-side connection to a client.
|
|
*
|
|
* This class provides the agent's view of an ACP connection, allowing
|
|
* agents to communicate with clients. It implements the {@link Client} interface
|
|
* to provide methods for requesting permissions, accessing the file system,
|
|
* and sending session updates.
|
|
*
|
|
* See protocol docs: [Agent](https://agentclientprotocol.com/protocol/overview#agent)
|
|
*/
|
|
export class AgentSideConnection {
|
|
#connection;
|
|
/**
|
|
* Creates a new agent-side connection to a client.
|
|
*
|
|
* This establishes the communication channel from the agent's perspective
|
|
* following the ACP specification.
|
|
*
|
|
* @param toAgent - A function that creates an Agent handler to process incoming client requests
|
|
* @param stream - The bidirectional message stream for communication. Typically created using
|
|
* {@link ndJsonStream} for stdio-based connections.
|
|
*
|
|
* See protocol docs: [Communication Model](https://agentclientprotocol.com/protocol/overview#communication-model)
|
|
*/
|
|
constructor(toAgent, stream) {
|
|
const agent = toAgent(this);
|
|
const requestHandler = async (method, params) => {
|
|
switch (method) {
|
|
case schema.AGENT_METHODS.initialize: {
|
|
const validatedParams = validate.zInitializeRequest.parse(params);
|
|
return agent.initialize(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_new: {
|
|
const validatedParams = validate.zNewSessionRequest.parse(params);
|
|
return agent.newSession(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_load: {
|
|
if (!agent.loadSession) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zLoadSessionRequest.parse(params);
|
|
return agent.loadSession(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_list: {
|
|
if (!agent.unstable_listSessions) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zListSessionsRequest.parse(params);
|
|
return agent.unstable_listSessions(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_fork: {
|
|
if (!agent.unstable_forkSession) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zForkSessionRequest.parse(params);
|
|
return agent.unstable_forkSession(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_resume: {
|
|
if (!agent.unstable_resumeSession) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zResumeSessionRequest.parse(params);
|
|
return agent.unstable_resumeSession(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_set_mode: {
|
|
if (!agent.setSessionMode) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zSetSessionModeRequest.parse(params);
|
|
const result = await agent.setSessionMode(validatedParams);
|
|
return result ?? {};
|
|
}
|
|
case schema.AGENT_METHODS.authenticate: {
|
|
const validatedParams = validate.zAuthenticateRequest.parse(params);
|
|
const result = await agent.authenticate(validatedParams);
|
|
return result ?? {};
|
|
}
|
|
case schema.AGENT_METHODS.session_prompt: {
|
|
const validatedParams = validate.zPromptRequest.parse(params);
|
|
return agent.prompt(validatedParams);
|
|
}
|
|
case schema.AGENT_METHODS.session_set_model: {
|
|
if (!agent.unstable_setSessionModel) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zSetSessionModelRequest.parse(params);
|
|
const result = await agent.unstable_setSessionModel(validatedParams);
|
|
return result ?? {};
|
|
}
|
|
case schema.AGENT_METHODS.session_set_config_option: {
|
|
if (!agent.unstable_setSessionConfigOption) {
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
const validatedParams = validate.zSetSessionConfigOptionRequest.parse(params);
|
|
return agent.unstable_setSessionConfigOption(validatedParams);
|
|
}
|
|
default:
|
|
if (agent.extMethod) {
|
|
return agent.extMethod(method, params);
|
|
}
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
};
|
|
const notificationHandler = async (method, params) => {
|
|
switch (method) {
|
|
case schema.AGENT_METHODS.session_cancel: {
|
|
const validatedParams = validate.zCancelNotification.parse(params);
|
|
return agent.cancel(validatedParams);
|
|
}
|
|
default:
|
|
if (agent.extNotification) {
|
|
return agent.extNotification(method, params);
|
|
}
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
};
|
|
this.#connection = new Connection(requestHandler, notificationHandler, stream);
|
|
}
|
|
/**
|
|
* Handles session update notifications from the agent.
|
|
*
|
|
* This is a notification endpoint (no response expected) that sends
|
|
* real-time updates about session progress, including message chunks,
|
|
* tool calls, and execution plans.
|
|
*
|
|
* Note: Clients SHOULD continue accepting tool call updates even after
|
|
* sending a `session/cancel` notification, as the agent may send final
|
|
* updates before responding with the cancelled stop reason.
|
|
*
|
|
* See protocol docs: [Agent Reports Output](https://agentclientprotocol.com/protocol/prompt-turn#3-agent-reports-output)
|
|
*/
|
|
async sessionUpdate(params) {
|
|
return await this.#connection.sendNotification(schema.CLIENT_METHODS.session_update, params);
|
|
}
|
|
/**
|
|
* Requests permission from the user for a tool call operation.
|
|
*
|
|
* Called by the agent when it needs user authorization before executing
|
|
* a potentially sensitive operation. The client should present the options
|
|
* to the user and return their decision.
|
|
*
|
|
* If the client cancels the prompt turn via `session/cancel`, it MUST
|
|
* respond to this request with `RequestPermissionOutcome::Cancelled`.
|
|
*
|
|
* See protocol docs: [Requesting Permission](https://agentclientprotocol.com/protocol/tool-calls#requesting-permission)
|
|
*/
|
|
async requestPermission(params) {
|
|
return await this.#connection.sendRequest(schema.CLIENT_METHODS.session_request_permission, params);
|
|
}
|
|
/**
|
|
* Reads content from a text file in the client's file system.
|
|
*
|
|
* Only available if the client advertises the `fs.readTextFile` capability.
|
|
* Allows the agent to access file contents within the client's environment.
|
|
*
|
|
* See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client)
|
|
*/
|
|
async readTextFile(params) {
|
|
return await this.#connection.sendRequest(schema.CLIENT_METHODS.fs_read_text_file, params);
|
|
}
|
|
/**
|
|
* Writes content to a text file in the client's file system.
|
|
*
|
|
* Only available if the client advertises the `fs.writeTextFile` capability.
|
|
* Allows the agent to create or modify files within the client's environment.
|
|
*
|
|
* See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client)
|
|
*/
|
|
async writeTextFile(params) {
|
|
return ((await this.#connection.sendRequest(schema.CLIENT_METHODS.fs_write_text_file, params)) ?? {});
|
|
}
|
|
/**
|
|
* Executes a command in a new terminal.
|
|
*
|
|
* Returns a `TerminalHandle` that can be used to get output, wait for exit,
|
|
* kill the command, or release the terminal.
|
|
*
|
|
* The terminal can also be embedded in tool calls by using its ID in
|
|
* `ToolCallContent` with type "terminal".
|
|
*
|
|
* @param params - The terminal creation parameters
|
|
* @returns A handle to control and monitor the terminal
|
|
*/
|
|
async createTerminal(params) {
|
|
const response = await this.#connection.sendRequest(schema.CLIENT_METHODS.terminal_create, params);
|
|
return new TerminalHandle(response.terminalId, params.sessionId, this.#connection);
|
|
}
|
|
/**
|
|
* Extension method
|
|
*
|
|
* Allows the Agent to send an arbitrary request that is not part of the ACP spec.
|
|
*/
|
|
async extMethod(method, params) {
|
|
return await this.#connection.sendRequest(method, params);
|
|
}
|
|
/**
|
|
* Extension notification
|
|
*
|
|
* Allows the Agent to send an arbitrary notification that is not part of the ACP spec.
|
|
*/
|
|
async extNotification(method, params) {
|
|
return await this.#connection.sendNotification(method, params);
|
|
}
|
|
/**
|
|
* AbortSignal that aborts when the connection closes.
|
|
*
|
|
* This signal can be used to:
|
|
* - Listen for connection closure: `connection.signal.addEventListener('abort', () => {...})`
|
|
* - Check connection status synchronously: `if (connection.signal.aborted) {...}`
|
|
* - Pass to other APIs (fetch, setTimeout) for automatic cancellation
|
|
*
|
|
* The connection closes when the underlying stream ends, either normally or due to an error.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const connection = new AgentSideConnection(agent, stream);
|
|
*
|
|
* // Listen for closure
|
|
* connection.signal.addEventListener('abort', () => {
|
|
* console.log('Connection closed - performing cleanup');
|
|
* });
|
|
*
|
|
* // Check status
|
|
* if (connection.signal.aborted) {
|
|
* console.log('Connection is already closed');
|
|
* }
|
|
*
|
|
* // Pass to other APIs
|
|
* fetch(url, { signal: connection.signal });
|
|
* ```
|
|
*/
|
|
get signal() {
|
|
return this.#connection.signal;
|
|
}
|
|
/**
|
|
* Promise that resolves when the connection closes.
|
|
*
|
|
* The connection closes when the underlying stream ends, either normally or due to an error.
|
|
* Once closed, the connection cannot send or receive any more messages.
|
|
*
|
|
* This is useful for async/await style cleanup:
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const connection = new AgentSideConnection(agent, stream);
|
|
* await connection.closed;
|
|
* console.log('Connection closed - performing cleanup');
|
|
* ```
|
|
*/
|
|
get closed() {
|
|
return this.#connection.closed;
|
|
}
|
|
}
|
|
/**
|
|
* Handle for controlling and monitoring a terminal created via `createTerminal`.
|
|
*
|
|
* Provides methods to:
|
|
* - Get current output without waiting
|
|
* - Wait for command completion
|
|
* - Kill the running command
|
|
* - Release terminal resources
|
|
*
|
|
* **Important:** Always call `release()` when done with the terminal to free resources.
|
|
|
|
* The terminal supports async disposal via `Symbol.asyncDispose` for automatic cleanup.
|
|
|
|
* You can use `await using` to ensure the terminal is automatically released when it
|
|
* goes out of scope.
|
|
*/
|
|
export class TerminalHandle {
|
|
id;
|
|
#sessionId;
|
|
#connection;
|
|
constructor(id, sessionId, conn) {
|
|
this.id = id;
|
|
this.#sessionId = sessionId;
|
|
this.#connection = conn;
|
|
}
|
|
/**
|
|
* Gets the current terminal output without waiting for the command to exit.
|
|
*/
|
|
async currentOutput() {
|
|
return await this.#connection.sendRequest(schema.CLIENT_METHODS.terminal_output, {
|
|
sessionId: this.#sessionId,
|
|
terminalId: this.id,
|
|
});
|
|
}
|
|
/**
|
|
* Waits for the terminal command to complete and returns its exit status.
|
|
*/
|
|
async waitForExit() {
|
|
return await this.#connection.sendRequest(schema.CLIENT_METHODS.terminal_wait_for_exit, {
|
|
sessionId: this.#sessionId,
|
|
terminalId: this.id,
|
|
});
|
|
}
|
|
/**
|
|
* Kills the terminal command without releasing the terminal.
|
|
*
|
|
* The terminal remains valid after killing, allowing you to:
|
|
* - Get the final output with `currentOutput()`
|
|
* - Check the exit status
|
|
* - Release the terminal when done
|
|
*
|
|
* Useful for implementing timeouts or cancellation.
|
|
*/
|
|
async kill() {
|
|
return ((await this.#connection.sendRequest(schema.CLIENT_METHODS.terminal_kill, {
|
|
sessionId: this.#sessionId,
|
|
terminalId: this.id,
|
|
})) ?? {});
|
|
}
|
|
/**
|
|
* Releases the terminal and frees all associated resources.
|
|
*
|
|
* If the command is still running, it will be killed.
|
|
* After release, the terminal ID becomes invalid and cannot be used
|
|
* with other terminal methods.
|
|
*
|
|
* Tool calls that already reference this terminal will continue to
|
|
* display its output.
|
|
*
|
|
* **Important:** Always call this method when done with the terminal.
|
|
*/
|
|
async release() {
|
|
return ((await this.#connection.sendRequest(schema.CLIENT_METHODS.terminal_release, {
|
|
sessionId: this.#sessionId,
|
|
terminalId: this.id,
|
|
})) ?? {});
|
|
}
|
|
async [Symbol.asyncDispose]() {
|
|
await this.release();
|
|
}
|
|
}
|
|
/**
|
|
* A client-side connection to an agent.
|
|
*
|
|
* This class provides the client's view of an ACP connection, allowing
|
|
* clients (such as code editors) to communicate with agents. It implements
|
|
* the {@link Agent} interface to provide methods for initializing sessions, sending
|
|
* prompts, and managing the agent lifecycle.
|
|
*
|
|
* See protocol docs: [Client](https://agentclientprotocol.com/protocol/overview#client)
|
|
*/
|
|
export class ClientSideConnection {
|
|
#connection;
|
|
/**
|
|
* Creates a new client-side connection to an agent.
|
|
*
|
|
* This establishes the communication channel between a client and agent
|
|
* following the ACP specification.
|
|
*
|
|
* @param toClient - A function that creates a Client handler to process incoming agent requests
|
|
* @param stream - The bidirectional message stream for communication. Typically created using
|
|
* {@link ndJsonStream} for stdio-based connections.
|
|
*
|
|
* See protocol docs: [Communication Model](https://agentclientprotocol.com/protocol/overview#communication-model)
|
|
*/
|
|
constructor(toClient, stream) {
|
|
const client = toClient(this);
|
|
const requestHandler = async (method, params) => {
|
|
switch (method) {
|
|
case schema.CLIENT_METHODS.fs_write_text_file: {
|
|
const validatedParams = validate.zWriteTextFileRequest.parse(params);
|
|
return client.writeTextFile?.(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.fs_read_text_file: {
|
|
const validatedParams = validate.zReadTextFileRequest.parse(params);
|
|
return client.readTextFile?.(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.session_request_permission: {
|
|
const validatedParams = validate.zRequestPermissionRequest.parse(params);
|
|
return client.requestPermission(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.terminal_create: {
|
|
const validatedParams = validate.zCreateTerminalRequest.parse(params);
|
|
return client.createTerminal?.(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.terminal_output: {
|
|
const validatedParams = validate.zTerminalOutputRequest.parse(params);
|
|
return client.terminalOutput?.(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.terminal_release: {
|
|
const validatedParams = validate.zReleaseTerminalRequest.parse(params);
|
|
const result = await client.releaseTerminal?.(validatedParams);
|
|
return result ?? {};
|
|
}
|
|
case schema.CLIENT_METHODS.terminal_wait_for_exit: {
|
|
const validatedParams = validate.zWaitForTerminalExitRequest.parse(params);
|
|
return client.waitForTerminalExit?.(validatedParams);
|
|
}
|
|
case schema.CLIENT_METHODS.terminal_kill: {
|
|
const validatedParams = validate.zKillTerminalCommandRequest.parse(params);
|
|
const result = await client.killTerminal?.(validatedParams);
|
|
return result ?? {};
|
|
}
|
|
default:
|
|
if (client.extMethod) {
|
|
return client.extMethod(method, params);
|
|
}
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
};
|
|
const notificationHandler = async (method, params) => {
|
|
switch (method) {
|
|
case schema.CLIENT_METHODS.session_update: {
|
|
const validatedParams = validate.zSessionNotification.parse(params);
|
|
return client.sessionUpdate(validatedParams);
|
|
}
|
|
default:
|
|
if (client.extNotification) {
|
|
return client.extNotification(method, params);
|
|
}
|
|
throw RequestError.methodNotFound(method);
|
|
}
|
|
};
|
|
this.#connection = new Connection(requestHandler, notificationHandler, stream);
|
|
}
|
|
/**
|
|
* Establishes the connection with a client and negotiates protocol capabilities.
|
|
*
|
|
* This method is called once at the beginning of the connection to:
|
|
* - Negotiate the protocol version to use
|
|
* - Exchange capability information between client and agent
|
|
* - Determine available authentication methods
|
|
*
|
|
* The agent should respond with its supported protocol version and capabilities.
|
|
*
|
|
* See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)
|
|
*/
|
|
async initialize(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.initialize, params);
|
|
}
|
|
/**
|
|
* Creates a new conversation session with the agent.
|
|
*
|
|
* Sessions represent independent conversation contexts with their own history and state.
|
|
*
|
|
* The agent should:
|
|
* - Create a new session context
|
|
* - Connect to any specified MCP servers
|
|
* - Return a unique session ID for future requests
|
|
*
|
|
* May return an `auth_required` error if the agent requires authentication.
|
|
*
|
|
* See protocol docs: [Session Setup](https://agentclientprotocol.com/protocol/session-setup)
|
|
*/
|
|
async newSession(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_new, params);
|
|
}
|
|
/**
|
|
* Loads an existing session to resume a previous conversation.
|
|
*
|
|
* This method is only available if the agent advertises the `loadSession` capability.
|
|
*
|
|
* The agent should:
|
|
* - Restore the session context and conversation history
|
|
* - Connect to the specified MCP servers
|
|
* - Stream the entire conversation history back to the client via notifications
|
|
*
|
|
* See protocol docs: [Loading Sessions](https://agentclientprotocol.com/protocol/session-setup#loading-sessions)
|
|
*/
|
|
async loadSession(params) {
|
|
return ((await this.#connection.sendRequest(schema.AGENT_METHODS.session_load, params)) ?? {});
|
|
}
|
|
/**
|
|
* **UNSTABLE**
|
|
*
|
|
* This capability is not part of the spec yet, and may be removed or changed at any point.
|
|
*
|
|
* Forks an existing session to create a new independent session.
|
|
*
|
|
* Creates a new session based on the context of an existing one, allowing
|
|
* operations like generating summaries without affecting the original session's history.
|
|
*
|
|
* This method is only available if the agent advertises the `session.fork` capability.
|
|
*
|
|
* @experimental
|
|
*/
|
|
async unstable_forkSession(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_fork, params);
|
|
}
|
|
/**
|
|
* **UNSTABLE**
|
|
*
|
|
* This capability is not part of the spec yet, and may be removed or changed at any point.
|
|
*
|
|
* Lists existing sessions from the agent.
|
|
*
|
|
* This method is only available if the agent advertises the `listSessions` capability.
|
|
*
|
|
* Returns a list of sessions with metadata like session ID, working directory,
|
|
* title, and last update time. Supports filtering by working directory and
|
|
* cursor-based pagination.
|
|
*
|
|
* @experimental
|
|
*/
|
|
async unstable_listSessions(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_list, params);
|
|
}
|
|
/**
|
|
* **UNSTABLE**
|
|
*
|
|
* This capability is not part of the spec yet, and may be removed or changed at any point.
|
|
*
|
|
* Resumes an existing session without returning previous messages.
|
|
*
|
|
* This method is only available if the agent advertises the `session.resume` capability.
|
|
*
|
|
* The agent should resume the session context, allowing the conversation to continue
|
|
* without replaying the message history (unlike `session/load`).
|
|
*
|
|
* @experimental
|
|
*/
|
|
async unstable_resumeSession(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_resume, params);
|
|
}
|
|
/**
|
|
* Sets the operational mode for a session.
|
|
*
|
|
* Allows switching between different agent modes (e.g., "ask", "architect", "code")
|
|
* that affect system prompts, tool availability, and permission behaviors.
|
|
*
|
|
* The mode must be one of the modes advertised in `availableModes` during session
|
|
* creation or loading. Agents may also change modes autonomously and notify the
|
|
* client via `current_mode_update` notifications.
|
|
*
|
|
* This method can be called at any time during a session, whether the Agent is
|
|
* idle or actively generating a turn.
|
|
*
|
|
* See protocol docs: [Session Modes](https://agentclientprotocol.com/protocol/session-modes)
|
|
*/
|
|
async setSessionMode(params) {
|
|
return ((await this.#connection.sendRequest(schema.AGENT_METHODS.session_set_mode, params)) ?? {});
|
|
}
|
|
/**
|
|
* **UNSTABLE**
|
|
*
|
|
* This capability is not part of the spec yet, and may be removed or changed at any point.
|
|
*
|
|
* Select a model for a given session.
|
|
*
|
|
* @experimental
|
|
*/
|
|
async unstable_setSessionModel(params) {
|
|
return ((await this.#connection.sendRequest(schema.AGENT_METHODS.session_set_model, params)) ?? {});
|
|
}
|
|
/**
|
|
* **UNSTABLE**
|
|
*
|
|
* This capability is not part of the spec yet, and may be removed or changed at any point.
|
|
*
|
|
* Set a configuration option for a given session.
|
|
*
|
|
* The response contains the full set of configuration options and their current values,
|
|
* as changing one option may affect the available values or state of other options.
|
|
*
|
|
* @experimental
|
|
*/
|
|
async unstable_setSessionConfigOption(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_set_config_option, params);
|
|
}
|
|
/**
|
|
* Authenticates the client using the specified authentication method.
|
|
*
|
|
* Called when the agent requires authentication before allowing session creation.
|
|
* The client provides the authentication method ID that was advertised during initialization.
|
|
*
|
|
* After successful authentication, the client can proceed to create sessions with
|
|
* `newSession` without receiving an `auth_required` error.
|
|
*
|
|
* See protocol docs: [Initialization](https://agentclientprotocol.com/protocol/initialization)
|
|
*/
|
|
async authenticate(params) {
|
|
return ((await this.#connection.sendRequest(schema.AGENT_METHODS.authenticate, params)) ?? {});
|
|
}
|
|
/**
|
|
* Processes a user prompt within a session.
|
|
*
|
|
* This method handles the whole lifecycle of a prompt:
|
|
* - Receives user messages with optional context (files, images, etc.)
|
|
* - Processes the prompt using language models
|
|
* - Reports language model content and tool calls to the Clients
|
|
* - Requests permission to run tools
|
|
* - Executes any requested tool calls
|
|
* - Returns when the turn is complete with a stop reason
|
|
*
|
|
* See protocol docs: [Prompt Turn](https://agentclientprotocol.com/protocol/prompt-turn)
|
|
*/
|
|
async prompt(params) {
|
|
return await this.#connection.sendRequest(schema.AGENT_METHODS.session_prompt, params);
|
|
}
|
|
/**
|
|
* Cancels ongoing operations for a session.
|
|
*
|
|
* This is a notification sent by the client to cancel an ongoing prompt turn.
|
|
*
|
|
* Upon receiving this notification, the Agent SHOULD:
|
|
* - Stop all language model requests as soon as possible
|
|
* - Abort all tool call invocations in progress
|
|
* - Send any pending `session/update` notifications
|
|
* - Respond to the original `session/prompt` request with `StopReason::Cancelled`
|
|
*
|
|
* See protocol docs: [Cancellation](https://agentclientprotocol.com/protocol/prompt-turn#cancellation)
|
|
*/
|
|
async cancel(params) {
|
|
return await this.#connection.sendNotification(schema.AGENT_METHODS.session_cancel, params);
|
|
}
|
|
/**
|
|
* Extension method
|
|
*
|
|
* Allows the Client to send an arbitrary request that is not part of the ACP spec.
|
|
*/
|
|
async extMethod(method, params) {
|
|
return await this.#connection.sendRequest(method, params);
|
|
}
|
|
/**
|
|
* Extension notification
|
|
*
|
|
* Allows the Client to send an arbitrary notification that is not part of the ACP spec.
|
|
*/
|
|
async extNotification(method, params) {
|
|
return await this.#connection.sendNotification(method, params);
|
|
}
|
|
/**
|
|
* AbortSignal that aborts when the connection closes.
|
|
*
|
|
* This signal can be used to:
|
|
* - Listen for connection closure: `connection.signal.addEventListener('abort', () => {...})`
|
|
* - Check connection status synchronously: `if (connection.signal.aborted) {...}`
|
|
* - Pass to other APIs (fetch, setTimeout) for automatic cancellation
|
|
*
|
|
* The connection closes when the underlying stream ends, either normally or due to an error.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const connection = new ClientSideConnection(client, stream);
|
|
*
|
|
* // Listen for closure
|
|
* connection.signal.addEventListener('abort', () => {
|
|
* console.log('Connection closed - performing cleanup');
|
|
* });
|
|
*
|
|
* // Check status
|
|
* if (connection.signal.aborted) {
|
|
* console.log('Connection is already closed');
|
|
* }
|
|
*
|
|
* // Pass to other APIs
|
|
* fetch(url, { signal: connection.signal });
|
|
* ```
|
|
*/
|
|
get signal() {
|
|
return this.#connection.signal;
|
|
}
|
|
/**
|
|
* Promise that resolves when the connection closes.
|
|
*
|
|
* The connection closes when the underlying stream ends, either normally or due to an error.
|
|
* Once closed, the connection cannot send or receive any more messages.
|
|
*
|
|
* This is useful for async/await style cleanup:
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const connection = new ClientSideConnection(client, stream);
|
|
* await connection.closed;
|
|
* console.log('Connection closed - performing cleanup');
|
|
* ```
|
|
*/
|
|
get closed() {
|
|
return this.#connection.closed;
|
|
}
|
|
}
|
|
class Connection {
|
|
#pendingResponses = new Map();
|
|
#nextRequestId = 0;
|
|
#requestHandler;
|
|
#notificationHandler;
|
|
#stream;
|
|
#writeQueue = Promise.resolve();
|
|
#abortController = new AbortController();
|
|
#closedPromise;
|
|
constructor(requestHandler, notificationHandler, stream) {
|
|
this.#requestHandler = requestHandler;
|
|
this.#notificationHandler = notificationHandler;
|
|
this.#stream = stream;
|
|
this.#closedPromise = new Promise((resolve) => {
|
|
this.#abortController.signal.addEventListener("abort", () => resolve());
|
|
});
|
|
this.#receive();
|
|
}
|
|
/**
|
|
* AbortSignal that aborts when the connection closes.
|
|
*
|
|
* This signal can be used to:
|
|
* - Listen for connection closure via event listeners
|
|
* - Check connection status synchronously with `signal.aborted`
|
|
* - Pass to other APIs (fetch, setTimeout) for automatic cancellation
|
|
*/
|
|
get signal() {
|
|
return this.#abortController.signal;
|
|
}
|
|
/**
|
|
* Promise that resolves when the connection closes.
|
|
*
|
|
* The connection closes when the underlying stream ends, either normally
|
|
* or due to an error. Once closed, the connection cannot send or receive
|
|
* any more messages.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const connection = new ClientSideConnection(client, stream);
|
|
* await connection.closed;
|
|
* console.log('Connection closed - performing cleanup');
|
|
* ```
|
|
*/
|
|
get closed() {
|
|
return this.#closedPromise;
|
|
}
|
|
async #receive() {
|
|
const reader = this.#stream.readable.getReader();
|
|
try {
|
|
while (true) {
|
|
const { value: message, done } = await reader.read();
|
|
if (done) {
|
|
break;
|
|
}
|
|
if (!message) {
|
|
continue;
|
|
}
|
|
try {
|
|
this.#processMessage(message);
|
|
}
|
|
catch (err) {
|
|
console.error("Unexpected error during message processing:", message, err);
|
|
// Only send error response if the message had an id (was a request)
|
|
if ("id" in message && message.id !== undefined) {
|
|
this.#sendMessage({
|
|
jsonrpc: "2.0",
|
|
id: message.id,
|
|
error: {
|
|
code: -32700,
|
|
message: "Parse error",
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally {
|
|
reader.releaseLock();
|
|
this.#abortController.abort();
|
|
}
|
|
}
|
|
async #processMessage(message) {
|
|
if ("method" in message && "id" in message) {
|
|
// It's a request
|
|
const response = await this.#tryCallRequestHandler(message.method, message.params);
|
|
if ("error" in response) {
|
|
console.error("Error handling request", message, response.error);
|
|
}
|
|
await this.#sendMessage({
|
|
jsonrpc: "2.0",
|
|
id: message.id,
|
|
...response,
|
|
});
|
|
}
|
|
else if ("method" in message) {
|
|
// It's a notification
|
|
const response = await this.#tryCallNotificationHandler(message.method, message.params);
|
|
if ("error" in response) {
|
|
console.error("Error handling notification", message, response.error);
|
|
}
|
|
}
|
|
else if ("id" in message) {
|
|
// It's a response
|
|
this.#handleResponse(message);
|
|
}
|
|
else {
|
|
console.error("Invalid message", { message });
|
|
}
|
|
}
|
|
async #tryCallRequestHandler(method, params) {
|
|
try {
|
|
const result = await this.#requestHandler(method, params);
|
|
return { result: result ?? null };
|
|
}
|
|
catch (error) {
|
|
if (error instanceof RequestError) {
|
|
return error.toResult();
|
|
}
|
|
if (error instanceof z.ZodError) {
|
|
return RequestError.invalidParams(error.format()).toResult();
|
|
}
|
|
let details;
|
|
if (error instanceof Error) {
|
|
details = error.message;
|
|
}
|
|
else if (typeof error === "object" &&
|
|
error != null &&
|
|
"message" in error &&
|
|
typeof error.message === "string") {
|
|
details = error.message;
|
|
}
|
|
try {
|
|
return RequestError.internalError(details ? JSON.parse(details) : {}).toResult();
|
|
}
|
|
catch {
|
|
return RequestError.internalError({ details }).toResult();
|
|
}
|
|
}
|
|
}
|
|
async #tryCallNotificationHandler(method, params) {
|
|
try {
|
|
await this.#notificationHandler(method, params);
|
|
return { result: null };
|
|
}
|
|
catch (error) {
|
|
if (error instanceof RequestError) {
|
|
return error.toResult();
|
|
}
|
|
if (error instanceof z.ZodError) {
|
|
return RequestError.invalidParams(error.format()).toResult();
|
|
}
|
|
let details;
|
|
if (error instanceof Error) {
|
|
details = error.message;
|
|
}
|
|
else if (typeof error === "object" &&
|
|
error != null &&
|
|
"message" in error &&
|
|
typeof error.message === "string") {
|
|
details = error.message;
|
|
}
|
|
try {
|
|
return RequestError.internalError(details ? JSON.parse(details) : {}).toResult();
|
|
}
|
|
catch {
|
|
return RequestError.internalError({ details }).toResult();
|
|
}
|
|
}
|
|
}
|
|
#handleResponse(response) {
|
|
const pendingResponse = this.#pendingResponses.get(response.id);
|
|
if (pendingResponse) {
|
|
if ("result" in response) {
|
|
pendingResponse.resolve(response.result);
|
|
}
|
|
else if ("error" in response) {
|
|
pendingResponse.reject(response.error);
|
|
}
|
|
this.#pendingResponses.delete(response.id);
|
|
}
|
|
else {
|
|
console.error("Got response to unknown request", response.id);
|
|
}
|
|
}
|
|
async sendRequest(method, params) {
|
|
const id = this.#nextRequestId++;
|
|
const responsePromise = new Promise((resolve, reject) => {
|
|
this.#pendingResponses.set(id, { resolve, reject });
|
|
});
|
|
await this.#sendMessage({ jsonrpc: "2.0", id, method, params });
|
|
return responsePromise;
|
|
}
|
|
async sendNotification(method, params) {
|
|
await this.#sendMessage({ jsonrpc: "2.0", method, params });
|
|
}
|
|
async #sendMessage(message) {
|
|
this.#writeQueue = this.#writeQueue
|
|
.then(async () => {
|
|
const writer = this.#stream.writable.getWriter();
|
|
try {
|
|
await writer.write(message);
|
|
}
|
|
finally {
|
|
writer.releaseLock();
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
// Continue processing writes on error
|
|
console.error("ACP write error:", error);
|
|
});
|
|
return this.#writeQueue;
|
|
}
|
|
}
|
|
/**
|
|
* JSON-RPC error object.
|
|
*
|
|
* Represents an error that occurred during method execution, following the
|
|
* JSON-RPC 2.0 error object specification with optional additional data.
|
|
*
|
|
* See protocol docs: [JSON-RPC Error Object](https://www.jsonrpc.org/specification#error_object)
|
|
*/
|
|
export class RequestError extends Error {
|
|
code;
|
|
data;
|
|
constructor(code, message, data) {
|
|
super(message);
|
|
this.code = code;
|
|
this.name = "RequestError";
|
|
this.data = data;
|
|
}
|
|
/**
|
|
* Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text.
|
|
*/
|
|
static parseError(data, additionalMessage) {
|
|
return new RequestError(-32700, `Parse error${additionalMessage ? `: ${additionalMessage}` : ""}`, data);
|
|
}
|
|
/**
|
|
* The JSON sent is not a valid Request object.
|
|
*/
|
|
static invalidRequest(data, additionalMessage) {
|
|
return new RequestError(-32600, `Invalid request${additionalMessage ? `: ${additionalMessage}` : ""}`, data);
|
|
}
|
|
/**
|
|
* The method does not exist / is not available.
|
|
*/
|
|
static methodNotFound(method) {
|
|
return new RequestError(-32601, `"Method not found": ${method}`, {
|
|
method,
|
|
});
|
|
}
|
|
/**
|
|
* Invalid method parameter(s).
|
|
*/
|
|
static invalidParams(data, additionalMessage) {
|
|
return new RequestError(-32602, `Invalid params${additionalMessage ? `: ${additionalMessage}` : ""}`, data);
|
|
}
|
|
/**
|
|
* Internal JSON-RPC error.
|
|
*/
|
|
static internalError(data, additionalMessage) {
|
|
return new RequestError(-32603, `Internal error${additionalMessage ? `: ${additionalMessage}` : ""}`, data);
|
|
}
|
|
/**
|
|
* Authentication required.
|
|
*/
|
|
static authRequired(data, additionalMessage) {
|
|
return new RequestError(-32000, `Authentication required${additionalMessage ? `: ${additionalMessage}` : ""}`, data);
|
|
}
|
|
/**
|
|
* Resource, such as a file, was not found
|
|
*/
|
|
static resourceNotFound(uri) {
|
|
return new RequestError(-32002, `Resource not found${uri ? `: ${uri}` : ""}`, uri && { uri });
|
|
}
|
|
toResult() {
|
|
return {
|
|
error: {
|
|
code: this.code,
|
|
message: this.message,
|
|
data: this.data,
|
|
},
|
|
};
|
|
}
|
|
toErrorResponse() {
|
|
return {
|
|
code: this.code,
|
|
message: this.message,
|
|
data: this.data,
|
|
};
|
|
}
|
|
}
|
|
//# sourceMappingURL=acp.js.map
|