Skip to main content

Overview

The ACP SDK allows agents to create terminals and execute commands in the client’s environment. This enables agents to run build tools, tests, scripts, and other command-line operations.
Terminal operations are only available if the client advertises the terminal capability during initialization.

Creating Terminals

The createTerminal method executes a command in a new terminal:
async createTerminal(
  params: CreateTerminalRequest
): Promise<TerminalHandle>
params.sessionId
string
required
The session ID creating the terminal
params.command
string
required
The command to execute
params.args
string[]
Optional command arguments
params.workingDirectory
string
Optional working directory for the command
params.env
Record<string, string>
Optional environment variables
TerminalHandle
object
required
A handle to control and monitor the terminal
Returns a TerminalHandle that can be used to get output, wait for exit, kill the command, or release the terminal.

Example

class MyAgent implements acp.Agent {
  private connection: acp.AgentSideConnection;

  async runCommand(
    sessionId: string,
    command: string,
    args: string[] = []
  ): Promise<string> {
    // Create the terminal
    const terminal = await this.connection.createTerminal({
      sessionId,
      command,
      args,
      workingDirectory: "/home/user/project",
    });

    try {
      // Wait for the command to complete
      const exitStatus = await terminal.waitForExit();

      // Get the final output
      const output = await terminal.currentOutput();

      if (exitStatus.exitCode === 0) {
        return output.output;
      } else {
        throw new Error(`Command failed with exit code ${exitStatus.exitCode}`);
      }
    } finally {
      // Always release the terminal
      await terminal.release();
    }
  }
}

TerminalHandle Class

The TerminalHandle class provides methods to control and monitor a terminal:
export class TerminalHandle {
  id: string;
  
  async currentOutput(): Promise<TerminalOutputResponse>;
  async waitForExit(): Promise<WaitForTerminalExitResponse>;
  async kill(): Promise<KillTerminalResponse>;
  async release(): Promise<ReleaseTerminalResponse | void>;
  [Symbol.asyncDispose](): Promise<void>;
}

currentOutput

Gets the current terminal output without waiting for the command to exit.
async currentOutput(): Promise<TerminalOutputResponse>
output
string
required
The current output from the terminal
exitCode
number
The exit code if the command has already exited
signal
string
The signal that terminated the process, if applicable

Example

const terminal = await this.connection.createTerminal({
  sessionId,
  command: "npm",
  args: ["install"],
});

// Poll for output while command is running
const pollInterval = setInterval(async () => {
  const output = await terminal.currentOutput();
  console.log("Current output:", output.output);
  
  if (output.exitCode !== undefined) {
    clearInterval(pollInterval);
  }
}, 1000);

waitForExit

Waits for the terminal command to complete and returns its exit status.
async waitForExit(): Promise<WaitForTerminalExitResponse>
exitCode
number
The exit code of the process
signal
string
The signal that terminated the process, if applicable
This method blocks until the command completes, providing the exit code and/or signal that terminated the process.

Example

const terminal = await this.connection.createTerminal({
  sessionId,
  command: "npm",
  args: ["test"],
});

// Wait for tests to complete
const exitStatus = await terminal.waitForExit();

if (exitStatus.exitCode === 0) {
  console.log("Tests passed!");
} else {
  console.error(`Tests failed with exit code ${exitStatus.exitCode}`);
}

await terminal.release();

kill

Kills the terminal command without releasing the terminal.
async kill(): Promise<KillTerminalResponse>
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.

Example

const terminal = await this.connection.createTerminal({
  sessionId,
  command: "long-running-command",
});

// Set a timeout
const timeout = setTimeout(async () => {
  console.log("Command timed out, killing...");
  await terminal.kill();
}, 30000); // 30 seconds

try {
  await terminal.waitForExit();
  clearTimeout(timeout);
} finally {
  // Get final output even after killing
  const output = await terminal.currentOutput();
  console.log("Final output:", output.output);
  
  await terminal.release();
}

release

Releases the terminal and frees all associated resources.
async release(): Promise<ReleaseTerminalResponse | void>
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.
Always call release() when done with the terminal to free resources.

Example with await using

The TerminalHandle class supports async disposal via Symbol.asyncDispose, allowing automatic cleanup:
// Automatically releases the terminal when it goes out of scope
await using terminal = await this.connection.createTerminal({
  sessionId,
  command: "npm",
  args: ["build"],
});

await terminal.waitForExit();
const output = await terminal.currentOutput();
// terminal.release() is called automatically

Embedding Terminals in Tool Calls

Terminals can be embedded in tool calls by using their ID in ToolCallContent with type “terminal”:
// Create a terminal
const terminal = await this.connection.createTerminal({
  sessionId,
  command: "npm",
  args: ["run", "build"],
});

// Send a tool call that references the terminal
await this.connection.sessionUpdate({
  sessionId,
  update: {
    sessionUpdate: "tool_call",
    toolCallId: "build_1",
    title: "Building project",
    kind: "bash",
    status: "pending",
    rawInput: { command: "npm run build" },
  },
});

// Wait for completion
const exitStatus = await terminal.waitForExit();
const output = await terminal.currentOutput();

// Update tool call with terminal output
await this.connection.sessionUpdate({
  sessionId,
  update: {
    sessionUpdate: "tool_call_update",
    toolCallId: "build_1",
    status: exitStatus.exitCode === 0 ? "completed" : "error",
    content: [
      {
        type: "terminal",
        terminalId: terminal.id,
      },
    ],
    rawOutput: { output: output.output, exitCode: exitStatus.exitCode },
  },
});

await terminal.release();

Error Handling

Handle errors appropriately when working with terminals:
async runCommandSafely(
  sessionId: string,
  command: string,
  args: string[]
): Promise<{ success: boolean; output: string; exitCode?: number }> {
  let terminal: acp.TerminalHandle | null = null;

  try {
    terminal = await this.connection.createTerminal({
      sessionId,
      command,
      args,
    });

    const exitStatus = await terminal.waitForExit();
    const output = await terminal.currentOutput();

    return {
      success: exitStatus.exitCode === 0,
      output: output.output,
      exitCode: exitStatus.exitCode,
    };
  } catch (error) {
    console.error("Terminal error:", error);
    return {
      success: false,
      output: error instanceof Error ? error.message : "Unknown error",
    };
  } finally {
    if (terminal) {
      await terminal.release();
    }
  }
}

Complete Example

Here’s a complete example that demonstrates terminal operations:
import * as acp from "@agentprotocol/acp";

class BuildAgent implements acp.Agent {
  private connection: acp.AgentSideConnection;
  private capabilities: acp.ClientCapabilities | null = null;

  constructor(connection: acp.AgentSideConnection) {
    this.connection = connection;
  }

  async initialize(params: acp.InitializeRequest): Promise<acp.InitializeResponse> {
    this.capabilities = params.clientCapabilities;
    return {
      protocolVersion: acp.PROTOCOL_VERSION,
      agentCapabilities: {},
    };
  }

  async runBuild(sessionId: string, workdir: string): Promise<void> {
    if (!this.capabilities?.terminal) {
      throw new Error("Terminal capability not available");
    }

    // Send initial message
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "agent_message_chunk",
        content: {
          type: "text",
          text: "Starting build process...",
        },
      },
    });

    // Create terminal for build
    await using terminal = await this.connection.createTerminal({
      sessionId,
      command: "npm",
      args: ["run", "build"],
      workingDirectory: workdir,
    });

    // Send tool call
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call",
        toolCallId: "build_1",
        title: "Running npm build",
        kind: "bash",
        status: "pending",
        rawInput: { command: "npm run build" },
      },
    });

    // Wait for completion
    const exitStatus = await terminal.waitForExit();
    const output = await terminal.currentOutput();

    // Update tool call with results
    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "tool_call_update",
        toolCallId: "build_1",
        status: exitStatus.exitCode === 0 ? "completed" : "error",
        content: [
          {
            type: "terminal",
            terminalId: terminal.id,
          },
        ],
        rawOutput: { output: output.output, exitCode: exitStatus.exitCode },
      },
    });

    // Send final message
    const message =
      exitStatus.exitCode === 0
        ? "Build completed successfully!"
        : `Build failed with exit code ${exitStatus.exitCode}`;

    await this.connection.sessionUpdate({
      sessionId,
      update: {
        sessionUpdate: "agent_message_chunk",
        content: { type: "text", text: message },
      },
    });
  }

  // ... implement other required Agent methods
}

Best Practices

Always Release

Always call release() when done to free resources, or use await using

Handle Timeouts

Implement timeouts using kill() to prevent commands from running indefinitely

Check Capabilities

Verify the client supports terminal operations before attempting to use them

Capture Output

Get final output after waitForExit() for complete command results

Build docs developers (and LLMs) love